commit
c166c92685
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
name: Bug/Problem report
|
||||
about: Create a report to help us improve Calibre-Web
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug/problem**
|
||||
A clear and concise description of what the bug is. If you are asking for support, please check our [Wiki](https://github.com/janeczku/calibre-web/wiki) if your question is already answered there.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS: [e.g. Windows 10/raspian]
|
||||
- Python version [e.g. python2.7]
|
||||
- Calibre-Web version [e.g. 0.6.5 or master@16.02.20, 19:55 ]:
|
||||
- Docker container [ None/Technosoft2000/Linuxuser]:
|
||||
- Special Hardware [e.g. Rasperry Pi Zero]
|
||||
- Browser [e.g. chrome, safari]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here. [e.g. access via reverse proxy]
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for Calibre-Web
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -21,14 +21,12 @@ vendor/
|
|||
# calibre-web
|
||||
*.db
|
||||
*.log
|
||||
config.ini
|
||||
cps/static/[0-9]*
|
||||
|
||||
.idea/
|
||||
*.bak
|
||||
*.log.*
|
||||
tags
|
||||
|
||||
settings.yaml
|
||||
gdrive_credentials
|
||||
client_secrets.json
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
|||
|
||||
## 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)
|
||||
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\
|
||||
|
@ -46,7 +46,7 @@ Please note that running the above install command can fail on some versions of
|
|||
|
||||
## 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:
|
||||
|
||||
|
|
11
cps.py
11
cps.py
|
@ -41,6 +41,14 @@ from cps.shelf import shelf
|
|||
from cps.admin import admi
|
||||
from cps.gdrive import gdrive
|
||||
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:
|
||||
from cps.oauth_bb import oauth
|
||||
oauth_available = True
|
||||
|
@ -58,6 +66,9 @@ def main():
|
|||
app.register_blueprint(admi)
|
||||
app.register_blueprint(gdrive)
|
||||
app.register_blueprint(editbook)
|
||||
if kobo_available:
|
||||
app.register_blueprint(kobo)
|
||||
app.register_blueprint(kobo_auth)
|
||||
if oauth_available:
|
||||
app.register_blueprint(oauth)
|
||||
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
|
||||
return user.locale
|
||||
|
||||
preferred = set()
|
||||
preferred = list()
|
||||
if request.accept_languages:
|
||||
for x in request.accept_languages.values():
|
||||
try:
|
||||
preferred.add(str(LC.parse(x.replace('-', '_'))))
|
||||
preferred.append(str(LC.parse(x.replace('-', '_'))))
|
||||
except (UnknownLocaleError, ValueError) as e:
|
||||
log.warning('Could not parse locale "%s": %s', x, e)
|
||||
# preferred.append('en')
|
||||
log.debug('Could not parse locale "%s": %s', x, e)
|
||||
|
||||
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
|
||||
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
|
||||
try:
|
||||
from flask_login import __version__ as flask_loginVersion
|
||||
|
@ -49,8 +49,11 @@ about = flask.Blueprint('about', __name__)
|
|||
|
||||
|
||||
_VERSIONS = OrderedDict(
|
||||
Platform = ' '.join(platform.uname()),
|
||||
Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
|
||||
Python=sys.version,
|
||||
Calibre_Web=constants.STABLE_VERSION['version'] + ' - '
|
||||
+ constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - '
|
||||
+ constants.NIGHTLY_VERSION[1].replace('%','%%'),
|
||||
WebServer=server.VERSION,
|
||||
Flask=flask.__version__,
|
||||
Flask_Login=flask_loginVersion,
|
||||
|
@ -67,7 +70,7 @@ _VERSIONS = OrderedDict(
|
|||
Unidecode = unidecode_version,
|
||||
Flask_SimpleLDAP = u'installed' if bool(services.ldap) 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())
|
||||
|
||||
|
|
291
cps/admin.py
291
cps/admin.py
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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 = {
|
||||
'ldap': False, # bool(services.ldap),
|
||||
'goodreads': bool(services.goodreads_support)
|
||||
'goodreads': bool(services.goodreads_support),
|
||||
'kobo': bool(services.kobo)
|
||||
}
|
||||
|
||||
# try:
|
||||
|
@ -144,7 +144,10 @@ def configuration():
|
|||
def view_configuration():
|
||||
readColumn = db.session.query(db.Custom_Columns)\
|
||||
.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,
|
||||
restrictColumns=restrictColumns,
|
||||
title=_(u"UI Configuration"), page="uiconfig")
|
||||
|
||||
|
||||
|
@ -160,7 +163,7 @@ def update_view_configuration():
|
|||
|
||||
_config_string("config_calibre_web_title")
|
||||
_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")
|
||||
|
||||
_config_int("config_read_column")
|
||||
|
@ -168,6 +171,7 @@ def update_view_configuration():
|
|||
_config_int("config_random_books")
|
||||
_config_int("config_books_per_page")
|
||||
_config_int("config_authors_max")
|
||||
_config_int("config_restricted_column")
|
||||
|
||||
if 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_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:
|
||||
config.config_default_show |= constants.DETAIL_RANDOM
|
||||
|
||||
|
@ -202,7 +204,6 @@ def edit_domain(allow):
|
|||
# value: 'superuser!' //new value
|
||||
vals = request.form.to_dict()
|
||||
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()
|
||||
ub.session.commit()
|
||||
return ""
|
||||
|
@ -247,6 +248,228 @@ def list_domain(allow):
|
|||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
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"])
|
||||
@unconfigured
|
||||
|
@ -262,7 +485,6 @@ def _configuration_update_helper():
|
|||
db_change = False
|
||||
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_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)
|
||||
|
@ -305,6 +527,9 @@ def _configuration_update_helper():
|
|||
_config_checkbox_int("config_uploading")
|
||||
_config_checkbox_int("config_anonbrowse")
|
||||
_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_string("config_calibre")
|
||||
|
@ -339,7 +564,7 @@ def _configuration_update_helper():
|
|||
# Remote login configuration
|
||||
_config_checkbox("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
|
||||
_config_checkbox("config_use_goodreads")
|
||||
|
@ -449,10 +674,11 @@ def new_user():
|
|||
content = ub.User()
|
||||
languages = speaking_language()
|
||||
translations = [LC('en')] + babel.list_translations()
|
||||
kobo_support = feature_support['kobo'] and config.config_kobo_sync
|
||||
if request.method == "POST":
|
||||
to_save = request.form.to_dict()
|
||||
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.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"]:
|
||||
flash(_(u"Please fill out all fields!"), category="error")
|
||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
||||
registered_oauth=oauth_check, 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"])
|
||||
existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower())\
|
||||
.first()
|
||||
|
@ -475,15 +702,20 @@ def new_user():
|
|||
if config.config_public_reg and not check_valid_domain(to_save["email"]):
|
||||
flash(_(u"E-mail is not from valid domain"), category="error")
|
||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
||||
registered_oauth=oauth_check, title=_(u"Add new user"))
|
||||
registered_oauth=oauth_check, kobo_support=kobo_support,
|
||||
title=_(u"Add new user"))
|
||||
else:
|
||||
content.email = to_save["email"]
|
||||
else:
|
||||
flash(_(u"Found an existing account for this e-mail address or nickname."), category="error")
|
||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||
registered_oauth=oauth_check)
|
||||
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||
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.commit()
|
||||
flash(_(u"User '%(user)s' created", user=content.nickname), category="success")
|
||||
|
@ -494,10 +726,9 @@ def new_user():
|
|||
else:
|
||||
content.role = config.config_default_role
|
||||
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,
|
||||
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")
|
||||
|
@ -552,6 +783,7 @@ def edit_user(user_id):
|
|||
downloads = list()
|
||||
languages = speaking_language()
|
||||
translations = babel.list_translations() + [LC('en')]
|
||||
kobo_support = feature_support['kobo'] and config.config_kobo_sync
|
||||
for book in content.downloads:
|
||||
downloadbook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
|
||||
if downloadbook:
|
||||
|
@ -597,8 +829,6 @@ def edit_user(user_id):
|
|||
else:
|
||||
content.sidebar_view &= ~constants.DETAIL_RANDOM
|
||||
|
||||
content.mature_content = "Show_mature_content" in to_save
|
||||
|
||||
if "default_language" in to_save:
|
||||
content.default_language = to_save["default_language"]
|
||||
if "locale" in to_save and to_save["locale"]:
|
||||
|
@ -610,9 +840,15 @@ def edit_user(user_id):
|
|||
content.email = to_save["email"]
|
||||
else:
|
||||
flash(_(u"Found an existing account for this e-mail address."), category="error")
|
||||
return render_title_template("user_edit.html", translations=translations, languages=languages,
|
||||
return render_title_template("user_edit.html",
|
||||
translations=translations,
|
||||
languages=languages,
|
||||
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")
|
||||
if "nickname" in to_save and to_save["nickname"] != content.nickname:
|
||||
# Query User nickname, if not existing, change
|
||||
|
@ -627,6 +863,7 @@ def edit_user(user_id):
|
|||
new_user=0, content=content,
|
||||
downloads=downloads,
|
||||
registered_oauth=oauth_check,
|
||||
kobo_support=kobo_support,
|
||||
title=_(u"Edit User %(nick)s", nick=content.nickname),
|
||||
page="edituser")
|
||||
|
||||
|
@ -638,9 +875,15 @@ def edit_user(user_id):
|
|||
except IntegrityError:
|
||||
ub.session.rollback()
|
||||
flash(_(u"An unknown error occured."), category="error")
|
||||
return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0,
|
||||
content=content, downloads=downloads, registered_oauth=oauth_check,
|
||||
return render_title_template("user_edit.html",
|
||||
translations=translations,
|
||||
languages=languages,
|
||||
new_user=0,
|
||||
content=content,
|
||||
downloads=downloads,
|
||||
registered_oauth=oauth_check,
|
||||
mail_configured=config.get_mail_server_configured(),
|
||||
kobo_support=kobo_support,
|
||||
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
|
||||
|
||||
|
||||
|
@ -671,8 +914,12 @@ def view_logfile():
|
|||
logfiles = {}
|
||||
logfiles[0] = logger.get_logfile(config.config_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,
|
||||
logfiles=logfiles, page="logfile")
|
||||
return render_title_template("logviewer.html",
|
||||
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>")
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# 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')
|
||||
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-web',
|
||||
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')
|
||||
args = parser.parse_args()
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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.ext.declarative import declarative_base
|
||||
|
||||
from . import constants, cli, logger
|
||||
from . import constants, cli, logger, ub
|
||||
|
||||
|
||||
log = logger.create()
|
||||
|
@ -68,12 +68,18 @@ class _Settings(_Base):
|
|||
config_anonbrowse = Column(SmallInteger, default=0)
|
||||
config_public_reg = Column(SmallInteger, default=0)
|
||||
config_remote_login = Column(Boolean, default=False)
|
||||
|
||||
config_kobo_sync = Column(Boolean, default=False)
|
||||
|
||||
config_default_role = Column(SmallInteger, default=0)
|
||||
config_default_show = Column(SmallInteger, default=6143)
|
||||
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_google_drive_folder = 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_oauth_provider = Column(Integer)
|
||||
config_kobo_proxy = Column(Boolean, default=False)
|
||||
|
||||
|
||||
config_ldap_provider_url = Column(String, default='localhost')
|
||||
config_ldap_port = Column(SmallInteger, default=389)
|
||||
|
@ -179,11 +186,20 @@ class _ConfigSQL(object):
|
|||
def show_detail_random(self):
|
||||
return self.show_element_new_user(constants.DETAIL_RANDOM)
|
||||
|
||||
def show_mature_content(self):
|
||||
return self.show_element_new_user(constants.MATURE_CONTENT)
|
||||
def list_denied_tags(self):
|
||||
mct = self.config_denied_tags.split(",")
|
||||
return [t.strip() for t in mct]
|
||||
|
||||
def mature_content_tags(self):
|
||||
mct = self.config_mature_content_tags.split(",")
|
||||
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 list_allowed_column_values(self):
|
||||
mct = self.config_allowed_column_value.split(",")
|
||||
return [t.strip() for t in mct]
|
||||
|
||||
def get_log_level(self):
|
||||
|
@ -323,5 +339,12 @@ def load_configuration(session):
|
|||
if not session.query(_Settings).count():
|
||||
session.add(_Settings())
|
||||
session.commit()
|
||||
|
||||
return _ConfigSQL(session)
|
||||
conf = _ConfigSQL(session)
|
||||
# Migrate from global restrictions to user based restrictions
|
||||
if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "":
|
||||
conf.config_denied_tags = conf.config_mature_content_tags
|
||||
conf.save()
|
||||
session.query(ub.User).filter(ub.User.mature_content != True). \
|
||||
update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False)
|
||||
session.commit()
|
||||
return conf
|
||||
|
|
|
@ -106,7 +106,6 @@ except ValueError:
|
|||
del env_CALIBRE_PORT
|
||||
|
||||
|
||||
|
||||
EXTENSIONS_AUDIO = {'mp3', 'm4a', 'm4b'}
|
||||
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',
|
||||
|
@ -126,7 +125,7 @@ def selected_roles(dictionary):
|
|||
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
||||
'series_id, languages')
|
||||
|
||||
STABLE_VERSION = {'version': '0.6.5 Beta'}
|
||||
STABLE_VERSION = {'version': '0.6.7 Beta'}
|
||||
|
||||
NIGHTLY_VERSION = {}
|
||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||
|
|
15
cps/db.py
15
cps/db.py
|
@ -25,13 +25,13 @@ import ast
|
|||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy import 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.ext.declarative import declarative_base
|
||||
|
||||
|
||||
session = None
|
||||
cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series']
|
||||
cc_exceptions = ['datetime', 'comments', 'composite', 'series']
|
||||
cc_classes = {}
|
||||
engine = None
|
||||
|
||||
|
@ -251,10 +251,10 @@ class Books(Base):
|
|||
title = Column(String)
|
||||
sort = Column(String)
|
||||
author_sort = Column(String)
|
||||
timestamp = Column(String)
|
||||
timestamp = Column(TIMESTAMP)
|
||||
pubdate = Column(String)
|
||||
series_index = Column(String)
|
||||
last_modified = Column(String)
|
||||
last_modified = Column(TIMESTAMP)
|
||||
path = Column(String)
|
||||
has_cover = Column(Integer)
|
||||
uuid = Column(String)
|
||||
|
@ -378,6 +378,11 @@ def setup_db(config):
|
|||
'id': Column(Integer, primary_key=True),
|
||||
'book': Column(Integer, ForeignKey('books.id')),
|
||||
'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:
|
||||
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
|
||||
'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)
|
||||
|
||||
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]],
|
||||
primaryjoin=(
|
||||
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)
|
||||
if not c.is_multiple:
|
||||
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]
|
||||
getattr(book, cc_string).remove(del_cc)
|
||||
db.session.delete(del_cc)
|
||||
|
@ -254,7 +254,7 @@ def edit_cc_data(book_id, book, to_save):
|
|||
else:
|
||||
cc_db_value = None
|
||||
if to_save[cc_string].strip():
|
||||
if c.datatype == 'int' or c.datatype == 'bool':
|
||||
if c.datatype == 'int' or c.datatype == 'bool' or c.datatype == 'float':
|
||||
if to_save[cc_string] == 'None':
|
||||
to_save[cc_string] = None
|
||||
elif c.datatype == 'bool':
|
||||
|
@ -369,11 +369,11 @@ def upload_cover(request, book):
|
|||
requested_file = request.files['btn-upload-cover']
|
||||
# check for empty request
|
||||
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
|
||||
else:
|
||||
# ToDo Message not always coorect
|
||||
flash(_(u"Cover is not a supported imageformat (jpg/png/webp), can't save"), category="error")
|
||||
flash(message, category="error")
|
||||
return False
|
||||
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
|
||||
# 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()
|
||||
|
||||
# upload book to gdrive if nesseccary and add "(bookid)" to folder name
|
||||
if config.config_use_google_drive:
|
||||
gdriveutils.updateGdriveCalibreFromLocal()
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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 -*-
|
||||
|
||||
# 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 -*-
|
||||
|
||||
# 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 -*-
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
def get_book_cover(book_id):
|
||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
||||
if book.has_cover:
|
||||
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):
|
||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
|
||||
return get_book_cover_internal(book, use_generic_cover_on_failure=True)
|
||||
|
||||
def get_book_cover_with_uuid(book_uuid,
|
||||
use_generic_cover_on_failure=True):
|
||||
book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first()
|
||||
return get_book_cover_internal(book, use_generic_cover_on_failure)
|
||||
|
||||
def get_book_cover_internal(book,
|
||||
use_generic_cover_on_failure):
|
||||
if book and book.has_cover:
|
||||
if config.config_use_google_drive:
|
||||
try:
|
||||
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)
|
||||
if path:
|
||||
return redirect(path)
|
||||
else:
|
||||
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:
|
||||
log.exception(e)
|
||||
# traceback.print_exc()
|
||||
return send_from_directory(_STATIC_DIR,"generic_cover.jpg")
|
||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||
else:
|
||||
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
|
||||
if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")):
|
||||
return send_from_directory(cover_file_path, "cover.jpg")
|
||||
else:
|
||||
return send_from_directory(_STATIC_DIR,"generic_cover.jpg")
|
||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||
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
|
||||
|
@ -494,16 +508,16 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
|
|||
os.makedirs(filepath)
|
||||
except OSError:
|
||||
log.error(u"Failed to create path for cover")
|
||||
return False
|
||||
return False, _(u"Failed to create path for cover")
|
||||
try:
|
||||
img.save(os.path.join(filepath, saved_filename))
|
||||
except IOError:
|
||||
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:
|
||||
log.error(u"Failed to store cover-file")
|
||||
return False
|
||||
return True
|
||||
return False, _(u"Failed to store cover-file")
|
||||
return True, None
|
||||
|
||||
|
||||
# saves book cover to gdrive or locally
|
||||
|
@ -513,7 +527,7 @@ def save_cover(img, book_path):
|
|||
if use_PIL:
|
||||
if content_type not in ('image/jpeg', 'image/png', 'image/webp'):
|
||||
log.error("Only jpg/jpeg/png/webp files are supported as coverfile")
|
||||
return False
|
||||
return False, _("Only jpg/jpeg/png/webp files are supported as coverfile")
|
||||
# convert to jpg because calibre only supports jpg
|
||||
if content_type in ('image/png', 'image/webp'):
|
||||
if hasattr(img,'stream'):
|
||||
|
@ -527,17 +541,18 @@ def save_cover(img, book_path):
|
|||
else:
|
||||
if content_type not in ('image/jpeg'):
|
||||
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:
|
||||
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'),
|
||||
os.path.join(tmpDir, "uploaded_cover.jpg"))
|
||||
log.info("Cover is saved on Google Drive")
|
||||
return True
|
||||
return True, None
|
||||
else:
|
||||
return False
|
||||
return False, message
|
||||
else:
|
||||
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())
|
||||
else:
|
||||
lang_filter = true()
|
||||
content_rating_filter = false() if current_user.mature_content else \
|
||||
db.Books.tags.any(db.Tags.name.in_(config.mature_content_tags()))
|
||||
return and_(lang_filter, ~content_rating_filter)
|
||||
negtags_list = current_user.list_denied_tags()
|
||||
postags_list = current_user.list_allowed_tags()
|
||||
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():
|
||||
return ~(false() if current_user.mature_content else \
|
||||
db.Tags.name.in_(config.mature_content_tags()))
|
||||
# return db.session.query(db.Tags).filter(~content_rating_filter).order_by(db.Tags.name).all()
|
||||
negtags_list = current_user.list_denied_tags()
|
||||
postags_list = current_user.list_allowed_tags()
|
||||
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
|
||||
def speaking_language(languages=None):
|
||||
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:
|
||||
try:
|
||||
cur_l = LC.parse(lang.lang_code)
|
||||
|
@ -774,7 +809,7 @@ def get_cc_columns():
|
|||
cc = []
|
||||
for col in tmpcc:
|
||||
r = re.compile(config.config_columns_to_ignore)
|
||||
if r.match(col.label):
|
||||
if not r.match(col.name):
|
||||
cc.append(col)
|
||||
else:
|
||||
cc = tmpcc
|
||||
|
@ -784,11 +819,11 @@ def get_download_link(book_id, book_format):
|
|||
book_format = book_format.split(".")[0]
|
||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
|
||||
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()
|
||||
else:
|
||||
abort(404)
|
||||
if data:
|
||||
if data1:
|
||||
# collect downloaded books only for registered user and not for anonymous user
|
||||
if current_user.is_authenticated:
|
||||
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)
|
||||
headers = Headers()
|
||||
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')),
|
||||
book_format)
|
||||
return do_download_file(book, book_format, data, headers)
|
||||
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (
|
||||
quote(file_name.encode('utf-8')), book_format, quote(file_name.encode('utf-8')), book_format)
|
||||
return do_download_file(book, book_format, data1, headers)
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
|
|
629
cps/kobo.py
Normal file
629
cps/kobo.py
Normal file
|
@ -0,0 +1,629 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2018-2019 shavitmichael, OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import base64
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from time import gmtime, strftime
|
||||
try:
|
||||
from urllib import unquote
|
||||
except ImportError:
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
request,
|
||||
make_response,
|
||||
jsonify,
|
||||
current_app,
|
||||
url_for,
|
||||
redirect,
|
||||
abort
|
||||
)
|
||||
from flask_login import login_required
|
||||
from werkzeug.datastructures import Headers
|
||||
from sqlalchemy import func
|
||||
import requests
|
||||
|
||||
from . import config, logger, kobo_auth, db, helper
|
||||
from .services import SyncToken as SyncToken
|
||||
from .web import download_required
|
||||
from .kobo_auth import requires_kobo_auth
|
||||
|
||||
KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]}
|
||||
KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
|
||||
|
||||
kobo = Blueprint("kobo", __name__, url_prefix="/kobo/<auth_token>")
|
||||
kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo)
|
||||
kobo_auth.register_url_value_preprocessor(kobo)
|
||||
|
||||
log = logger.create()
|
||||
|
||||
def get_store_url_for_current_request():
|
||||
# Programmatically modify the current url to point to the official Kobo store
|
||||
base, sep, request_path_with_auth_token = request.full_path.rpartition("/kobo/")
|
||||
auth_token, sep, request_path = request_path_with_auth_token.rstrip("?").partition(
|
||||
"/"
|
||||
)
|
||||
return KOBO_STOREAPI_URL + "/" + request_path
|
||||
|
||||
|
||||
CONNECTION_SPECIFIC_HEADERS = [
|
||||
"connection",
|
||||
"content-encoding",
|
||||
"content-length",
|
||||
"transfer-encoding",
|
||||
]
|
||||
|
||||
def get_kobo_activated():
|
||||
return config.config_kobo_sync
|
||||
|
||||
|
||||
def make_request_to_kobo_store(sync_token=None):
|
||||
outgoing_headers = Headers(request.headers)
|
||||
outgoing_headers.remove("Host")
|
||||
if sync_token:
|
||||
sync_token.set_kobo_store_header(outgoing_headers)
|
||||
|
||||
store_response = requests.request(
|
||||
method=request.method,
|
||||
url=get_store_url_for_current_request(),
|
||||
headers=outgoing_headers,
|
||||
data=request.get_data(),
|
||||
allow_redirects=False,
|
||||
timeout=(2, 10)
|
||||
)
|
||||
return store_response
|
||||
|
||||
|
||||
def redirect_or_proxy_request():
|
||||
if config.config_kobo_proxy:
|
||||
if request.method == "GET":
|
||||
return redirect(get_store_url_for_current_request(), 307)
|
||||
if request.method == "DELETE":
|
||||
log.info('Delete Book')
|
||||
return make_response(jsonify({}))
|
||||
else:
|
||||
# The Kobo device turns other request types into GET requests on redirects, so we instead proxy to the Kobo store ourselves.
|
||||
store_response = make_request_to_kobo_store()
|
||||
|
||||
response_headers = store_response.headers
|
||||
for header_key in CONNECTION_SPECIFIC_HEADERS:
|
||||
response_headers.pop(header_key, default=None)
|
||||
|
||||
return make_response(
|
||||
store_response.content, store_response.status_code, response_headers.items()
|
||||
)
|
||||
else:
|
||||
return make_response(jsonify({}))
|
||||
|
||||
|
||||
@kobo.route("/v1/library/sync")
|
||||
@requires_kobo_auth
|
||||
@download_required
|
||||
def HandleSyncRequest():
|
||||
sync_token = SyncToken.SyncToken.from_headers(request.headers)
|
||||
log.info("Kobo library sync request received.")
|
||||
if not current_app.wsgi_app.is_proxied:
|
||||
log.debug('Kobo: Received unproxied request, changed request port to server port')
|
||||
|
||||
# TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header
|
||||
# instead so that the device triggers another sync.
|
||||
|
||||
new_books_last_modified = sync_token.books_last_modified
|
||||
new_books_last_created = sync_token.books_last_created
|
||||
entitlements = []
|
||||
|
||||
# We reload the book database so that the user get's a fresh view of the library
|
||||
# in case of external changes (e.g: adding a book through Calibre).
|
||||
db.reconnect_db(config)
|
||||
|
||||
# sqlite gives unexpected results when performing the last_modified comparison without the datetime cast.
|
||||
# It looks like it's treating the db.Books.last_modified field as a string and may fail
|
||||
# the comparison because of the +00:00 suffix.
|
||||
changed_entries = (
|
||||
db.session.query(db.Books)
|
||||
.join(db.Data)
|
||||
.filter(func.datetime(db.Books.last_modified) > sync_token.books_last_modified)
|
||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||
.all()
|
||||
)
|
||||
|
||||
for book in changed_entries:
|
||||
entitlement = {
|
||||
"BookEntitlement": create_book_entitlement(book),
|
||||
"BookMetadata": get_metadata(book),
|
||||
"ReadingState": reading_state(book),
|
||||
}
|
||||
if book.timestamp > sync_token.books_last_created:
|
||||
entitlements.append({"NewEntitlement": entitlement})
|
||||
else:
|
||||
entitlements.append({"ChangedEntitlement": entitlement})
|
||||
|
||||
new_books_last_modified = max(
|
||||
book.last_modified, sync_token.books_last_modified
|
||||
)
|
||||
new_books_last_created = max(book.timestamp, sync_token.books_last_created)
|
||||
|
||||
sync_token.books_last_created = new_books_last_created
|
||||
sync_token.books_last_modified = new_books_last_modified
|
||||
|
||||
if config.config_kobo_proxy:
|
||||
return generate_sync_response(request, sync_token, entitlements)
|
||||
|
||||
return make_response(jsonify(entitlements))
|
||||
# Missing feature: Detect server-side book deletions.
|
||||
|
||||
|
||||
def generate_sync_response(request, sync_token, entitlements):
|
||||
extra_headers = {}
|
||||
if config.config_kobo_proxy:
|
||||
# Merge in sync results from the official Kobo store.
|
||||
try:
|
||||
store_response = make_request_to_kobo_store(sync_token)
|
||||
|
||||
store_entitlements = store_response.json()
|
||||
entitlements += store_entitlements
|
||||
sync_token.merge_from_store_response(store_response)
|
||||
extra_headers["x-kobo-sync"] = store_response.headers.get("x-kobo-sync")
|
||||
extra_headers["x-kobo-sync-mode"] = store_response.headers.get("x-kobo-sync-mode")
|
||||
extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads")
|
||||
|
||||
except Exception as e:
|
||||
log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e))
|
||||
sync_token.to_headers(extra_headers)
|
||||
|
||||
response = make_response(jsonify(entitlements), extra_headers)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@kobo.route("/v1/library/<book_uuid>/metadata")
|
||||
@requires_kobo_auth
|
||||
@download_required
|
||||
def HandleMetadataRequest(book_uuid):
|
||||
if not current_app.wsgi_app.is_proxied:
|
||||
log.debug('Kobo: Received unproxied request, changed request port to server port')
|
||||
log.info("Kobo library metadata request received for book %s" % book_uuid)
|
||||
book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first()
|
||||
if not book or not book.data:
|
||||
log.info(u"Book %s not found in database", book_uuid)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
metadata = get_metadata(book)
|
||||
return jsonify([metadata])
|
||||
|
||||
|
||||
def get_download_url_for_book(book, book_format):
|
||||
if not current_app.wsgi_app.is_proxied:
|
||||
if ':' in request.host and not request.host.endswith(']') :
|
||||
host = "".join(request.host.split(':')[:-1])
|
||||
else:
|
||||
host = request.host
|
||||
return "{url_scheme}://{url_base}:{url_port}/download/{book_id}/{book_format}".format(
|
||||
url_scheme=request.scheme,
|
||||
url_base=host,
|
||||
url_port=config.config_port,
|
||||
book_id=book.id,
|
||||
book_format=book_format.lower()
|
||||
)
|
||||
return url_for(
|
||||
"web.download_link",
|
||||
book_id=book.id,
|
||||
book_format=book_format.lower(),
|
||||
_external=True,
|
||||
)
|
||||
|
||||
|
||||
def create_book_entitlement(book):
|
||||
book_uuid = book.uuid
|
||||
return {
|
||||
"Accessibility": "Full",
|
||||
"ActivePeriod": {"From": current_time(),},
|
||||
"Created": book.timestamp.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"CrossRevisionId": book_uuid,
|
||||
"Id": book_uuid,
|
||||
"IsHiddenFromArchive": False,
|
||||
"IsLocked": False,
|
||||
# Setting this to true removes from the device.
|
||||
"IsRemoved": False,
|
||||
"LastModified": book.last_modified.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"OriginCategory": "Imported",
|
||||
"RevisionId": book_uuid,
|
||||
"Status": "Active",
|
||||
}
|
||||
|
||||
|
||||
def current_time():
|
||||
return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime())
|
||||
|
||||
|
||||
def get_description(book):
|
||||
if not book.comments:
|
||||
return None
|
||||
return book.comments[0].text
|
||||
|
||||
|
||||
# TODO handle multiple authors
|
||||
def get_author(book):
|
||||
if not book.authors:
|
||||
return None
|
||||
return book.authors[0].name
|
||||
|
||||
|
||||
def get_publisher(book):
|
||||
if not book.publishers:
|
||||
return None
|
||||
return book.publishers[0].name
|
||||
|
||||
|
||||
def get_series(book):
|
||||
if not book.series:
|
||||
return None
|
||||
return book.series[0].name
|
||||
|
||||
|
||||
def get_metadata(book):
|
||||
download_urls = []
|
||||
for book_data in book.data:
|
||||
if book_data.format not in KOBO_FORMATS:
|
||||
continue
|
||||
for kobo_format in KOBO_FORMATS[book_data.format]:
|
||||
# log.debug('Id: %s, Format: %s' % (book.id, kobo_format))
|
||||
download_urls.append(
|
||||
{
|
||||
"Format": kobo_format,
|
||||
"Size": book_data.uncompressed_size,
|
||||
"Url": get_download_url_for_book(book, book_data.format),
|
||||
# The Kobo forma accepts platforms: (Generic, Android)
|
||||
"Platform": "Generic",
|
||||
# "DrmType": "None", # Not required
|
||||
}
|
||||
)
|
||||
|
||||
book_uuid = book.uuid
|
||||
metadata = {
|
||||
"Categories": ["00000000-0000-0000-0000-000000000001",],
|
||||
"Contributors": get_author(book),
|
||||
"CoverImageId": book_uuid,
|
||||
"CrossRevisionId": book_uuid,
|
||||
"CurrentDisplayPrice": {"CurrencyCode": "USD", "TotalAmount": 0},
|
||||
"CurrentLoveDisplayPrice": {"TotalAmount": 0},
|
||||
"Description": get_description(book),
|
||||
"DownloadUrls": download_urls,
|
||||
"EntitlementId": book_uuid,
|
||||
"ExternalIds": [],
|
||||
"Genre": "00000000-0000-0000-0000-000000000001",
|
||||
"IsEligibleForKoboLove": False,
|
||||
"IsInternetArchive": False,
|
||||
"IsPreOrder": False,
|
||||
"IsSocialEnabled": True,
|
||||
"Language": "en",
|
||||
"PhoneticPronunciations": {},
|
||||
"PublicationDate": book.pubdate,
|
||||
"Publisher": {"Imprint": "", "Name": get_publisher(book),},
|
||||
"RevisionId": book_uuid,
|
||||
"Title": book.title,
|
||||
"WorkId": book_uuid,
|
||||
}
|
||||
|
||||
if get_series(book):
|
||||
if sys.version_info < (3, 0):
|
||||
name = get_series(book).encode("utf-8")
|
||||
else:
|
||||
name = get_series(book)
|
||||
metadata["Series"] = {
|
||||
"Name": get_series(book),
|
||||
"Number": book.series_index,
|
||||
"NumberFloat": float(book.series_index),
|
||||
# Get a deterministic id based on the series name.
|
||||
"Id": uuid.uuid3(uuid.NAMESPACE_DNS, name),
|
||||
}
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def reading_state(book):
|
||||
# TODO: Implement
|
||||
reading_state = {
|
||||
# "StatusInfo": {
|
||||
# "LastModified": get_single_cc_value(book, "lastreadtimestamp"),
|
||||
# "Status": get_single_cc_value(book, "reading_status"),
|
||||
# }
|
||||
# TODO: CurrentBookmark, Location
|
||||
}
|
||||
return reading_state
|
||||
|
||||
|
||||
@kobo.route("/<book_uuid>/image.jpg")
|
||||
@requires_kobo_auth
|
||||
def HandleCoverImageRequest(book_uuid):
|
||||
book_cover = helper.get_book_cover_with_uuid(
|
||||
book_uuid, use_generic_cover_on_failure=False
|
||||
)
|
||||
if not book_cover:
|
||||
if config.config_kobo_proxy:
|
||||
log.debug("Cover for unknown book: %s proxied to kobo" % book_uuid)
|
||||
return redirect(get_store_url_for_current_request(), 307)
|
||||
else:
|
||||
log.debug("Cover for unknown book: %s requested" % book_uuid)
|
||||
return redirect_or_proxy_request()
|
||||
log.debug("Cover request received for book %s" % book_uuid)
|
||||
return book_cover
|
||||
|
||||
|
||||
@kobo.route("")
|
||||
def TopLevelEndpoint():
|
||||
return make_response(jsonify({}))
|
||||
|
||||
|
||||
# TODO: Implement the following routes
|
||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
|
||||
@kobo.route("/v1/library/<book_uuid>/state", methods=["PUT"])
|
||||
@kobo.route("/v1/library/tags", methods=["POST"])
|
||||
@kobo.route("/v1/library/tags/<shelf_name>", methods=["POST"])
|
||||
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE"])
|
||||
def HandleUnimplementedRequest(dummy=None, book_uuid=None, shelf_name=None, tag_id=None):
|
||||
log.debug("Unimplemented Library Request received: %s", request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
|
||||
# TODO: Implement the following routes
|
||||
@kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/user/profile", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/user/recommendations", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
|
||||
def HandleUserRequest(dummy=None):
|
||||
log.debug("Unimplemented User Request received: %s", request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
|
||||
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/<dummy>/reviews", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/books/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/dailydeal", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products", methods=["GET", "POST"])
|
||||
def HandleProductsRequest(dummy=None):
|
||||
log.debug("Unimplemented Products Request received: %s", request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
|
||||
@kobo.app_errorhandler(404)
|
||||
def handle_404(err):
|
||||
# This handler acts as a catch-all for endpoints that we don't have an interest in
|
||||
# implementing (e.g: v1/analytics/gettests, v1/user/recommendations, etc)
|
||||
log.debug("Unknown Request received: %s", request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
|
||||
def make_calibre_web_auth_response():
|
||||
# As described in kobo_auth.py, CalibreWeb doesn't make use practical use of this auth/device API call for
|
||||
# authentation (nor for authorization). We return a dummy response just to keep the device happy.
|
||||
content = request.get_json()
|
||||
AccessToken = base64.b64encode(os.urandom(24)).decode('utf-8')
|
||||
RefreshToken = base64.b64encode(os.urandom(24)).decode('utf-8')
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"AccessToken": AccessToken,
|
||||
"RefreshToken": RefreshToken,
|
||||
"TokenType": "Bearer",
|
||||
"TrackingId": str(uuid.uuid4()),
|
||||
"UserKey": content['UserKey'],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@kobo.route("/v1/auth/device", methods=["POST"])
|
||||
@requires_kobo_auth
|
||||
def HandleAuthRequest():
|
||||
log.debug('Kobo Auth request')
|
||||
if config.config_kobo_proxy:
|
||||
try:
|
||||
return redirect_or_proxy_request()
|
||||
except:
|
||||
log.error("Failed to receive or parse response from Kobo's auth endpoint. Falling back to un-proxied mode.")
|
||||
return make_calibre_web_auth_response()
|
||||
|
||||
|
||||
def make_calibre_web_init_response(calibre_web_url):
|
||||
resources = NATIVE_KOBO_RESOURCES(calibre_web_url)
|
||||
response = make_response(jsonify({"Resources": resources}))
|
||||
response.headers["x-kobo-apitoken"] = "e30="
|
||||
return response
|
||||
|
||||
|
||||
@kobo.route("/v1/initialization")
|
||||
@requires_kobo_auth
|
||||
def HandleInitRequest():
|
||||
log.info('Init')
|
||||
|
||||
if not current_app.wsgi_app.is_proxied:
|
||||
log.debug('Kobo: Received unproxied request, changed request port to server port')
|
||||
if ':' in request.host and not request.host.endswith(']'):
|
||||
host = "".join(request.host.split(':')[:-1])
|
||||
else:
|
||||
host = request.host
|
||||
calibre_web_url = "{url_scheme}://{url_base}:{url_port}".format(
|
||||
url_scheme=request.scheme,
|
||||
url_base=host,
|
||||
url_port=config.config_port
|
||||
)
|
||||
else:
|
||||
calibre_web_url = url_for("web.index", _external=True).strip("/")
|
||||
|
||||
if config.config_kobo_proxy:
|
||||
try:
|
||||
store_response = make_request_to_kobo_store()
|
||||
|
||||
store_response_json = store_response.json()
|
||||
if "Resources" in store_response_json:
|
||||
kobo_resources = store_response_json["Resources"]
|
||||
# calibre_web_url = url_for("web.index", _external=True).strip("/")
|
||||
kobo_resources["image_host"] = calibre_web_url
|
||||
kobo_resources["image_url_quality_template"] = unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
|
||||
auth_token = kobo_auth.get_auth_token(),
|
||||
book_uuid="{ImageId}"))
|
||||
kobo_resources["image_url_template"] = unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
|
||||
auth_token = kobo_auth.get_auth_token(),
|
||||
book_uuid="{ImageId}"))
|
||||
|
||||
return make_response(store_response_json, store_response.status_code)
|
||||
except:
|
||||
log.error("Failed to receive or parse response from Kobo's init endpoint. Falling back to un-proxied mode.")
|
||||
|
||||
return make_calibre_web_init_response(calibre_web_url)
|
||||
|
||||
|
||||
def NATIVE_KOBO_RESOURCES(calibre_web_url):
|
||||
return {
|
||||
"account_page": "https://secure.kobobooks.com/profile",
|
||||
"account_page_rakuten": "https://my.rakuten.co.jp/",
|
||||
"add_entitlement": "https://storeapi.kobo.com/v1/library/{RevisionIds}",
|
||||
"affiliaterequest": "https://storeapi.kobo.com/v1/affiliate",
|
||||
"audiobook_subscription_orange_deal_inclusion_url": "https://authorize.kobo.com/inclusion",
|
||||
"authorproduct_recommendations": "https://storeapi.kobo.com/v1/products/books/authors/recommendations",
|
||||
"autocomplete": "https://storeapi.kobo.com/v1/products/autocomplete",
|
||||
"blackstone_header": {"key": "x-amz-request-payer", "value": "requester"},
|
||||
"book": "https://storeapi.kobo.com/v1/products/books/{ProductId}",
|
||||
"book_detail_page": "https://store.kobobooks.com/{culture}/ebook/{slug}",
|
||||
"book_detail_page_rakuten": "http://books.rakuten.co.jp/rk/{crossrevisionid}",
|
||||
"book_landing_page": "https://store.kobobooks.com/ebooks",
|
||||
"book_subscription": "https://storeapi.kobo.com/v1/products/books/subscriptions",
|
||||
"categories": "https://storeapi.kobo.com/v1/categories",
|
||||
"categories_page": "https://store.kobobooks.com/ebooks/categories",
|
||||
"category": "https://storeapi.kobo.com/v1/categories/{CategoryId}",
|
||||
"category_featured_lists": "https://storeapi.kobo.com/v1/categories/{CategoryId}/featured",
|
||||
"category_products": "https://storeapi.kobo.com/v1/categories/{CategoryId}/products",
|
||||
"checkout_borrowed_book": "https://storeapi.kobo.com/v1/library/borrow",
|
||||
"configuration_data": "https://storeapi.kobo.com/v1/configuration",
|
||||
"content_access_book": "https://storeapi.kobo.com/v1/products/books/{ProductId}/access",
|
||||
"customer_care_live_chat": "https://v2.zopim.com/widget/livechat.html?key=Y6gwUmnu4OATxN3Tli4Av9bYN319BTdO",
|
||||
"daily_deal": "https://storeapi.kobo.com/v1/products/dailydeal",
|
||||
"deals": "https://storeapi.kobo.com/v1/deals",
|
||||
"delete_entitlement": "https://storeapi.kobo.com/v1/library/{Ids}",
|
||||
"delete_tag": "https://storeapi.kobo.com/v1/library/tags/{TagId}",
|
||||
"delete_tag_items": "https://storeapi.kobo.com/v1/library/tags/{TagId}/items/delete",
|
||||
"device_auth": "https://storeapi.kobo.com/v1/auth/device",
|
||||
"device_refresh": "https://storeapi.kobo.com/v1/auth/refresh",
|
||||
"dictionary_host": "https://kbdownload1-a.akamaihd.net",
|
||||
"discovery_host": "https://discovery.kobobooks.com",
|
||||
"eula_page": "https://www.kobo.com/termsofuse?style=onestore",
|
||||
"exchange_auth": "https://storeapi.kobo.com/v1/auth/exchange",
|
||||
"external_book": "https://storeapi.kobo.com/v1/products/books/external/{Ids}",
|
||||
"facebook_sso_page": "https://authorize.kobo.com/signin/provider/Facebook/login?returnUrl=http://store.kobobooks.com/",
|
||||
"featured_list": "https://storeapi.kobo.com/v1/products/featured/{FeaturedListId}",
|
||||
"featured_lists": "https://storeapi.kobo.com/v1/products/featured",
|
||||
"free_books_page": {
|
||||
"EN": "https://www.kobo.com/{region}/{language}/p/free-ebooks",
|
||||
"FR": "https://www.kobo.com/{region}/{language}/p/livres-gratuits",
|
||||
"IT": "https://www.kobo.com/{region}/{language}/p/libri-gratuiti",
|
||||
"NL": "https://www.kobo.com/{region}/{language}/List/bekijk-het-overzicht-van-gratis-ebooks/QpkkVWnUw8sxmgjSlCbJRg",
|
||||
"PT": "https://www.kobo.com/{region}/{language}/p/livros-gratis",
|
||||
},
|
||||
"fte_feedback": "https://storeapi.kobo.com/v1/products/ftefeedback",
|
||||
"get_tests_request": "https://storeapi.kobo.com/v1/analytics/gettests",
|
||||
"giftcard_epd_redeem_url": "https://www.kobo.com/{storefront}/{language}/redeem-ereader",
|
||||
"giftcard_redeem_url": "https://www.kobo.com/{storefront}/{language}/redeem",
|
||||
"help_page": "http://www.kobo.com/help",
|
||||
"image_host": calibre_web_url,
|
||||
"image_url_quality_template": unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
|
||||
auth_token = kobo_auth.get_auth_token(),
|
||||
book_uuid="{ImageId}")),
|
||||
"image_url_template": unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
|
||||
auth_token = kobo_auth.get_auth_token(),
|
||||
book_uuid="{ImageId}")),
|
||||
"kobo_audiobooks_enabled": "False",
|
||||
"kobo_audiobooks_orange_deal_enabled": "False",
|
||||
"kobo_audiobooks_subscriptions_enabled": "False",
|
||||
"kobo_nativeborrow_enabled": "True",
|
||||
"kobo_onestorelibrary_enabled": "False",
|
||||
"kobo_redeem_enabled": "True",
|
||||
"kobo_shelfie_enabled": "False",
|
||||
"kobo_subscriptions_enabled": "False",
|
||||
"kobo_superpoints_enabled": "False",
|
||||
"kobo_wishlist_enabled": "True",
|
||||
"library_book": "https://storeapi.kobo.com/v1/user/library/books/{LibraryItemId}",
|
||||
"library_items": "https://storeapi.kobo.com/v1/user/library",
|
||||
"library_metadata": "https://storeapi.kobo.com/v1/library/{Ids}/metadata",
|
||||
"library_prices": "https://storeapi.kobo.com/v1/user/library/previews/prices",
|
||||
"library_stack": "https://storeapi.kobo.com/v1/user/library/stacks/{LibraryItemId}",
|
||||
"library_sync": "https://storeapi.kobo.com/v1/library/sync",
|
||||
"love_dashboard_page": "https://store.kobobooks.com/{culture}/kobosuperpoints",
|
||||
"love_points_redemption_page": "https://store.kobobooks.com/{culture}/KoboSuperPointsRedemption?productId={ProductId}",
|
||||
"magazine_landing_page": "https://store.kobobooks.com/emagazines",
|
||||
"notifications_registration_issue": "https://storeapi.kobo.com/v1/notifications/registration",
|
||||
"oauth_host": "https://oauth.kobo.com",
|
||||
"overdrive_account": "https://auth.overdrive.com/account",
|
||||
"overdrive_library": "https://{libraryKey}.auth.overdrive.com/library",
|
||||
"overdrive_library_finder_host": "https://libraryfinder.api.overdrive.com",
|
||||
"overdrive_thunder_host": "https://thunder.api.overdrive.com",
|
||||
"password_retrieval_page": "https://www.kobobooks.com/passwordretrieval.html",
|
||||
"post_analytics_event": "https://storeapi.kobo.com/v1/analytics/event",
|
||||
"privacy_page": "https://www.kobo.com/privacypolicy?style=onestore",
|
||||
"product_nextread": "https://storeapi.kobo.com/v1/products/{ProductIds}/nextread",
|
||||
"product_prices": "https://storeapi.kobo.com/v1/products/{ProductIds}/prices",
|
||||
"product_recommendations": "https://storeapi.kobo.com/v1/products/{ProductId}/recommendations",
|
||||
"product_reviews": "https://storeapi.kobo.com/v1/products/{ProductIds}/reviews",
|
||||
"products": "https://storeapi.kobo.com/v1/products",
|
||||
"provider_external_sign_in_page": "https://authorize.kobo.com/ExternalSignIn/{providerName}?returnUrl=http://store.kobobooks.com/",
|
||||
"purchase_buy": "https://www.kobo.com/checkout/createpurchase/",
|
||||
"purchase_buy_templated": "https://www.kobo.com/{culture}/checkout/createpurchase/{ProductId}",
|
||||
"quickbuy_checkout": "https://storeapi.kobo.com/v1/store/quickbuy/{PurchaseId}/checkout",
|
||||
"quickbuy_create": "https://storeapi.kobo.com/v1/store/quickbuy/purchase",
|
||||
"rating": "https://storeapi.kobo.com/v1/products/{ProductId}/rating/{Rating}",
|
||||
"reading_state": "https://storeapi.kobo.com/v1/library/{Ids}/state",
|
||||
"redeem_interstitial_page": "https://store.kobobooks.com",
|
||||
"registration_page": "https://authorize.kobo.com/signup?returnUrl=http://store.kobobooks.com/",
|
||||
"related_items": "https://storeapi.kobo.com/v1/products/{Id}/related",
|
||||
"remaining_book_series": "https://storeapi.kobo.com/v1/products/books/series/{SeriesId}",
|
||||
"rename_tag": "https://storeapi.kobo.com/v1/library/tags/{TagId}",
|
||||
"review": "https://storeapi.kobo.com/v1/products/reviews/{ReviewId}",
|
||||
"review_sentiment": "https://storeapi.kobo.com/v1/products/reviews/{ReviewId}/sentiment/{Sentiment}",
|
||||
"shelfie_recommendations": "https://storeapi.kobo.com/v1/user/recommendations/shelfie",
|
||||
"sign_in_page": "https://authorize.kobo.com/signin?returnUrl=http://store.kobobooks.com/",
|
||||
"social_authorization_host": "https://social.kobobooks.com:8443",
|
||||
"social_host": "https://social.kobobooks.com",
|
||||
"stacks_host_productId": "https://store.kobobooks.com/collections/byproductid/",
|
||||
"store_home": "www.kobo.com/{region}/{language}",
|
||||
"store_host": "store.kobobooks.com",
|
||||
"store_newreleases": "https://store.kobobooks.com/{culture}/List/new-releases/961XUjtsU0qxkFItWOutGA",
|
||||
"store_search": "https://store.kobobooks.com/{culture}/Search?Query={query}",
|
||||
"store_top50": "https://store.kobobooks.com/{culture}/ebooks/Top",
|
||||
"tag_items": "https://storeapi.kobo.com/v1/library/tags/{TagId}/Items",
|
||||
"tags": "https://storeapi.kobo.com/v1/library/tags",
|
||||
"taste_profile": "https://storeapi.kobo.com/v1/products/tasteprofile",
|
||||
"update_accessibility_to_preview": "https://storeapi.kobo.com/v1/library/{EntitlementIds}/preview",
|
||||
"use_one_store": "False",
|
||||
"user_loyalty_benefits": "https://storeapi.kobo.com/v1/user/loyalty/benefits",
|
||||
"user_platform": "https://storeapi.kobo.com/v1/user/platform",
|
||||
"user_profile": "https://storeapi.kobo.com/v1/user/profile",
|
||||
"user_ratings": "https://storeapi.kobo.com/v1/user/ratings",
|
||||
"user_recommendations": "https://storeapi.kobo.com/v1/user/recommendations",
|
||||
"user_reviews": "https://storeapi.kobo.com/v1/user/reviews",
|
||||
"user_wishlist": "https://storeapi.kobo.com/v1/user/wishlist",
|
||||
"userguide_host": "https://kbdownload1-a.akamaihd.net",
|
||||
"wishlist_page": "https://store.kobobooks.com/{region}/{language}/account/wishlist",
|
||||
}
|
165
cps/kobo_auth.py
Normal file
165
cps/kobo_auth.py
Normal file
|
@ -0,0 +1,165 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2018-2019 shavitmichael, OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""This module is used to control authentication/authorization of Kobo sync requests.
|
||||
This module also includes research notes into the auth protocol used by Kobo devices.
|
||||
|
||||
Log-in:
|
||||
When first booting a Kobo device the user must sign into a Kobo (or affiliate) account.
|
||||
Upon successful sign-in, the user is redirected to
|
||||
https://auth.kobobooks.com/CrossDomainSignIn?id=<some id>
|
||||
which serves the following response:
|
||||
<script type='text/javascript'>
|
||||
location.href='kobo://UserAuthenticated?userId=<redacted>&userKey<redacted>&email=<redacted>&returnUrl=https%3a%2f%2fwww.kobo.com';
|
||||
</script>
|
||||
And triggers the insertion of a userKey into the device's User table.
|
||||
|
||||
Together, the device's DeviceId and UserKey act as an *irrevocable* authentication
|
||||
token to most (if not all) Kobo APIs. In fact, in most cases only the UserKey is
|
||||
required to authorize the API call.
|
||||
|
||||
Changing Kobo password *does not* invalidate user keys! This is apparently a known
|
||||
issue for a few years now https://www.mobileread.com/forums/showpost.php?p=3476851&postcount=13
|
||||
(although this poster hypothesised that Kobo could blacklist a DeviceId, many endpoints
|
||||
will still grant access given the userkey.)
|
||||
|
||||
Official Kobo Store Api authorization:
|
||||
* For most of the endpoints we care about (sync, metadata, tags, etc), the userKey is
|
||||
passed in the x-kobo-userkey header, and is sufficient to authorize the API call.
|
||||
* Some endpoints (e.g: AnnotationService) instead make use of Bearer tokens pass through
|
||||
an authorization header. To get a BearerToken, the device makes a POST request to the
|
||||
v1/auth/device endpoint with the secret UserKey and the device's DeviceId.
|
||||
* The book download endpoint passes an auth token as a URL param instead of a header.
|
||||
|
||||
Our implementation:
|
||||
We pretty much ignore all of the above. To authenticate the user, we generate a random
|
||||
and unique token that they append to the CalibreWeb Url when setting up the api_store
|
||||
setting on the device.
|
||||
Thus, every request from the device to the api_store will hit CalibreWeb with the
|
||||
auth_token in the url (e.g: https://mylibrary.com/<auth_token>/v1/library/sync).
|
||||
In addition, once authenticated we also set the login cookie on the response that will
|
||||
be sent back for the duration of the session to authorize subsequent API calls (in
|
||||
particular calls to non-Kobo specific endpoints such as the CalibreWeb book download).
|
||||
"""
|
||||
|
||||
from binascii import hexlify
|
||||
from datetime import datetime
|
||||
from os import urandom
|
||||
import os
|
||||
|
||||
from flask import g, Blueprint, url_for, abort, request
|
||||
from flask_login import login_user, login_required
|
||||
from flask_babel import gettext as _
|
||||
|
||||
from . import logger, ub, lm
|
||||
from .web import render_title_template
|
||||
|
||||
try:
|
||||
from functools import wraps
|
||||
except ImportError:
|
||||
pass # We're not using Python 3
|
||||
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
def register_url_value_preprocessor(kobo):
|
||||
@kobo.url_value_preprocessor
|
||||
def pop_auth_token(endpoint, values):
|
||||
g.auth_token = values.pop("auth_token")
|
||||
|
||||
|
||||
def disable_failed_auth_redirect_for_blueprint(bp):
|
||||
lm.blueprint_login_views[bp.name] = None
|
||||
|
||||
|
||||
def get_auth_token():
|
||||
if "auth_token" in g:
|
||||
return g.get("auth_token")
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def requires_kobo_auth(f):
|
||||
@wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
auth_token = get_auth_token()
|
||||
if auth_token is not None:
|
||||
user = (
|
||||
ub.session.query(ub.User)
|
||||
.join(ub.RemoteAuthToken)
|
||||
.filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1)
|
||||
.first()
|
||||
)
|
||||
if user is not None:
|
||||
login_user(user)
|
||||
return f(*args, **kwargs)
|
||||
log.debug("Received Kobo request without a recognizable auth token.")
|
||||
return abort(401)
|
||||
return inner
|
||||
|
||||
|
||||
kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
|
||||
|
||||
|
||||
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
||||
@login_required
|
||||
def generate_auth_token(user_id):
|
||||
host = ':'.join(request.host.rsplit(':')[0:-1])
|
||||
if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f'):
|
||||
warning = _('PLease access calibre-web from non localhost to get valid api_endpoint for kobo device')
|
||||
return render_title_template(
|
||||
"generate_kobo_auth_url.html",
|
||||
title=_(u"Kobo Setup"),
|
||||
warning = warning
|
||||
)
|
||||
else:
|
||||
# Invalidate any prevously generated Kobo Auth token for this user.
|
||||
auth_token = ub.session.query(ub.RemoteAuthToken).filter(
|
||||
ub.RemoteAuthToken.user_id == user_id
|
||||
).filter(ub.RemoteAuthToken.token_type==1).first()
|
||||
|
||||
if not auth_token:
|
||||
auth_token = ub.RemoteAuthToken()
|
||||
auth_token.user_id = user_id
|
||||
auth_token.expiration = datetime.max
|
||||
auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
|
||||
auth_token.token_type = 1
|
||||
|
||||
ub.session.add(auth_token)
|
||||
ub.session.commit()
|
||||
return render_title_template(
|
||||
"generate_kobo_auth_url.html",
|
||||
title=_(u"Kobo Setup"),
|
||||
kobo_auth_url=url_for(
|
||||
"kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True
|
||||
),
|
||||
warning = False
|
||||
)
|
||||
|
||||
|
||||
@kobo_auth.route("/deleteauthtoken/<int:user_id>")
|
||||
@login_required
|
||||
def delete_auth_token(user_id):
|
||||
# Invalidate any prevously generated Kobo Auth token for this user.
|
||||
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
||||
.filter(ub.RemoteAuthToken.token_type==1).delete()
|
||||
ub.session.commit()
|
||||
return ""
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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 -*-
|
||||
|
||||
# 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):
|
||||
if config.config_login_type == constants.LOGIN_OAUTH:
|
||||
return f(*args, **kwargs)
|
||||
if request.is_xhr:
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
data = {'status': 'error', 'message': 'Not Found'}
|
||||
response = make_response(json.dumps(data, ensure_ascii=False))
|
||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
|
|
60
cps/opds.py
60
cps/opds.py
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
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")
|
||||
@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])
|
||||
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")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_formatindex():
|
||||
|
@ -223,10 +261,11 @@ def feed_formatindex():
|
|||
.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,
|
||||
len(entries))
|
||||
|
||||
element = list()
|
||||
for entry in entries:
|
||||
entry.name = entry.format
|
||||
entry.id = entry.format
|
||||
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_format', pagination=pagination)
|
||||
element.append(FeedObject(entry.format, entry.format))
|
||||
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_format', pagination=pagination)
|
||||
|
||||
|
||||
@opds.route("/opds/formats/<book_id>")
|
||||
|
@ -266,16 +305,9 @@ def feed_languages(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.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)
|
||||
|
||||
|
||||
@opds.route("/opds/shelfindex", defaults={'public': 0})
|
||||
@opds.route("/opds/shelfindex/<string:public>")
|
||||
@requires_basic_auth_if_no_ano
|
||||
|
@ -320,11 +352,11 @@ def feed_shelf(book_id):
|
|||
@requires_basic_auth_if_no_ano
|
||||
@download_required
|
||||
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>")
|
||||
@opds.route("/ajax/book/<string:uuid>",defaults={'library': ""})
|
||||
@requires_basic_auth_if_no_ano
|
||||
def get_metadata_calibre_companion(uuid, library):
|
||||
entry = db.session.query(db.Books).filter(db.Books.uuid.like("%" + uuid + "%")).first()
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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 -*-
|
||||
|
||||
# Flask License
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Flask License
|
||||
|
@ -60,10 +59,13 @@ class ReverseProxied(object):
|
|||
|
||||
def __init__(self, application):
|
||||
self.app = application
|
||||
self.proxied = False
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
self.proxied = False
|
||||
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
|
||||
if script_name:
|
||||
self.proxied = True
|
||||
environ['SCRIPT_NAME'] = script_name
|
||||
path_info = environ.get('PATH_INFO', '')
|
||||
if path_info and path_info.startswith(script_name):
|
||||
|
@ -76,3 +78,7 @@ class ReverseProxied(object):
|
|||
if servr:
|
||||
environ['HTTP_HOST'] = servr
|
||||
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
|
||||
|
||||
def _start_tornado(self):
|
||||
if os.name == 'nt':
|
||||
if os.name == 'nt' and sys.version_info > (3, 7):
|
||||
import asyncio
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
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,
|
||||
ssl_options=self.ssl_args)
|
||||
http_server.listen(self.listen_port, self.listen_address)
|
||||
self.wsgiserver = IOLoop.instance()
|
||||
self.wsgiserver = IOLoop.current()
|
||||
self.wsgiserver.start()
|
||||
# wait for stop signal
|
||||
self.wsgiserver.close(True)
|
||||
|
@ -177,6 +177,8 @@ class WebServer(object):
|
|||
|
||||
if not self.restart:
|
||||
log.info("Performing shutdown of Calibre-Web")
|
||||
# prevent irritiating log of pending tasks message from asyncio
|
||||
logger.get('asyncio').setLevel(logger.logging.CRITICAL)
|
||||
return True
|
||||
|
||||
log.info("Performing restart of Calibre-Web")
|
||||
|
@ -197,4 +199,4 @@ class WebServer(object):
|
|||
if _GEVENT:
|
||||
self.wsgiserver.close()
|
||||
else:
|
||||
self.wsgiserver.add_callback(self.wsgiserver.stop)
|
||||
self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop)
|
||||
|
|
148
cps/services/SyncToken.py
Normal file
148
cps/services/SyncToken.py
Normal file
|
@ -0,0 +1,148 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2018-2019 shavitmichael, OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
from base64 import b64decode, b64encode
|
||||
from jsonschema import validate, exceptions, __version__
|
||||
from datetime import datetime
|
||||
try:
|
||||
from urllib import unquote
|
||||
except ImportError:
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import json
|
||||
from .. import logger as log
|
||||
|
||||
|
||||
def b64encode_json(json_data):
|
||||
if sys.version_info < (3, 0):
|
||||
return b64encode(json.dumps(json_data))
|
||||
else:
|
||||
return b64encode(json.dumps(json_data).encode())
|
||||
|
||||
|
||||
# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2.
|
||||
def to_epoch_timestamp(datetime_object):
|
||||
return (datetime_object - datetime(1970, 1, 1)).total_seconds()
|
||||
|
||||
|
||||
class SyncToken():
|
||||
""" The SyncToken is used to persist state accross requests.
|
||||
When serialized over the response headers, the Kobo device will propagate the token onto following requests to the service.
|
||||
As an example use-case, the SyncToken is used to detect books that have been added to the library since the last time the device synced to the server.
|
||||
|
||||
Attributes:
|
||||
books_last_created: Datetime representing the newest book that the device knows about.
|
||||
books_last_modified: Datetime representing the last modified book that the device knows about.
|
||||
"""
|
||||
|
||||
SYNC_TOKEN_HEADER = "x-kobo-synctoken"
|
||||
VERSION = "1-0-0"
|
||||
MIN_VERSION = "1-0-0"
|
||||
|
||||
token_schema = {
|
||||
"type": "object",
|
||||
"properties": {"version": {"type": "string"}, "data": {"type": "object"},},
|
||||
}
|
||||
# This Schema doesn't contain enough information to detect and propagate book deletions from Calibre to the device.
|
||||
# A potential solution might be to keep a list of all known book uuids in the token, and look for any missing from the db.
|
||||
data_schema_v1 = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"raw_kobo_store_token": {"type": "string"},
|
||||
"books_last_modified": {"type": "string"},
|
||||
"books_last_created": {"type": "string"},
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
raw_kobo_store_token="",
|
||||
books_last_created=datetime.min,
|
||||
books_last_modified=datetime.min,
|
||||
):
|
||||
self.raw_kobo_store_token = raw_kobo_store_token
|
||||
self.books_last_created = books_last_created
|
||||
self.books_last_modified = books_last_modified
|
||||
|
||||
@staticmethod
|
||||
def from_headers(headers):
|
||||
sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "")
|
||||
if sync_token_header == "":
|
||||
return SyncToken()
|
||||
|
||||
# On the first sync from a Kobo device, we may receive the SyncToken
|
||||
# from the official Kobo store. Without digging too deep into it, that
|
||||
# token is of the form [b64encoded blob].[b64encoded blob 2]
|
||||
if "." in sync_token_header:
|
||||
return SyncToken(raw_kobo_store_token=sync_token_header)
|
||||
|
||||
try:
|
||||
sync_token_json = json.loads(
|
||||
b64decode(sync_token_header + "=" * (-len(sync_token_header) % 4))
|
||||
)
|
||||
validate(sync_token_json, SyncToken.token_schema)
|
||||
if sync_token_json["version"] < SyncToken.MIN_VERSION:
|
||||
raise ValueError
|
||||
|
||||
data_json = sync_token_json["data"]
|
||||
validate(sync_token_json, SyncToken.data_schema_v1)
|
||||
except (exceptions.ValidationError, ValueError):
|
||||
log.error("Sync token contents do not follow the expected json schema.")
|
||||
return SyncToken()
|
||||
|
||||
raw_kobo_store_token = data_json["raw_kobo_store_token"]
|
||||
try:
|
||||
books_last_modified = datetime.utcfromtimestamp(
|
||||
data_json["books_last_modified"]
|
||||
)
|
||||
books_last_created = datetime.utcfromtimestamp(
|
||||
data_json["books_last_created"]
|
||||
)
|
||||
except TypeError:
|
||||
log.error("SyncToken timestamps don't parse to a datetime.")
|
||||
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
||||
|
||||
return SyncToken(
|
||||
raw_kobo_store_token=raw_kobo_store_token,
|
||||
books_last_created=books_last_created,
|
||||
books_last_modified=books_last_modified,
|
||||
)
|
||||
|
||||
def set_kobo_store_header(self, store_headers):
|
||||
store_headers.set(SyncToken.SYNC_TOKEN_HEADER, self.raw_kobo_store_token)
|
||||
|
||||
def merge_from_store_response(self, store_response):
|
||||
self.raw_kobo_store_token = store_response.headers.get(
|
||||
SyncToken.SYNC_TOKEN_HEADER, ""
|
||||
)
|
||||
|
||||
def to_headers(self, headers):
|
||||
headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token()
|
||||
|
||||
def build_sync_token(self):
|
||||
token = {
|
||||
"version": SyncToken.VERSION,
|
||||
"data": {
|
||||
"raw_kobo_store_token": self.raw_kobo_store_token,
|
||||
"books_last_modified": to_epoch_timestamp(self.books_last_modified),
|
||||
"books_last_created": to_epoch_timestamp(self.books_last_created),
|
||||
},
|
||||
}
|
||||
return b64encode_json(token)
|
|
@ -35,4 +35,10 @@ except ImportError as err:
|
|||
log.debug("cannot import simpleldap, logging in with ldap will not work: %s", err)
|
||||
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
|
||||
|
|
52
cps/shelf.py
52
cps/shelf.py
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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>")
|
||||
@login_required
|
||||
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()
|
||||
if shelf is None:
|
||||
log.error("Invalid shelf specified: %s", shelf_id)
|
||||
if not request.is_xhr:
|
||||
if not xhr:
|
||||
flash(_(u"Invalid shelf specified"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
return "Invalid shelf specified", 400
|
||||
|
||||
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)
|
||||
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),
|
||||
category="error")
|
||||
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():
|
||||
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")
|
||||
return redirect(url_for('web.index'))
|
||||
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()
|
||||
if book_in_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")
|
||||
return redirect(url_for('web.index'))
|
||||
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)
|
||||
ub.session.add(ins)
|
||||
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")
|
||||
if "HTTP_REFERER" in request.environ:
|
||||
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>")
|
||||
@login_required
|
||||
def remove_from_shelf(shelf_id, book_id):
|
||||
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
if shelf is None:
|
||||
log.error("Invalid shelf specified: %s", shelf_id)
|
||||
if not request.is_xhr:
|
||||
if not xhr:
|
||||
return redirect(url_for('web.index'))
|
||||
return "Invalid shelf specified", 400
|
||||
|
||||
|
@ -169,20 +170,23 @@ def remove_from_shelf(shelf_id, book_id):
|
|||
|
||||
if book_shelf is None:
|
||||
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 "Book already removed from shelf", 410
|
||||
|
||||
ub.session.delete(book_shelf)
|
||||
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")
|
||||
if "HTTP_REFERER" in request.environ:
|
||||
return redirect(request.environ["HTTP_REFERER"])
|
||||
else:
|
||||
return redirect(url_for('web.index'))
|
||||
return "", 204
|
||||
else:
|
||||
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),
|
||||
category="error")
|
||||
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)\
|
||||
.order_by(ub.BookShelf.order.asc()).all()
|
||||
books_list = [ b.book_id for b in books_in_shelf]
|
||||
result = db.session.query(db.Books).filter(db.Books.id.in_(books_list)).filter(common_filters()).all()
|
||||
for book in books_in_shelf:
|
||||
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),
|
||||
shelf=shelf, page="shelf")
|
||||
else:
|
||||
|
@ -317,8 +329,20 @@ def order_shelf(shelf_id):
|
|||
if shelf:
|
||||
books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
|
||||
.order_by(ub.BookShelf.order.asc()).all()
|
||||
books_list = [ b.book_id for b in books_in_shelf2]
|
||||
result = db.session.query(db.Books).filter(db.Books.id.in_(books_list)).filter(common_filters()).all()
|
||||
for book in books_in_shelf2:
|
||||
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,
|
||||
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
|
||||
shelf=shelf, page="shelforder")
|
||||
|
|
76
cps/static/css/libs/viewer.css
vendored
76
cps/static/css/libs/viewer.css
vendored
|
@ -230,36 +230,46 @@
|
|||
z-index: 200;
|
||||
max-width: 20em;
|
||||
background-color: #FFFF99;
|
||||
box-shadow: 0px 2px 5px #333;
|
||||
box-shadow: 0px 2px 5px #888;
|
||||
border-radius: 2px;
|
||||
padding: 0.6em;
|
||||
padding: 6px;
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
font: message-box;
|
||||
font-size: 9px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.annotationLayer .popup > * {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.annotationLayer .popup h1 {
|
||||
font-size: 1em;
|
||||
border-bottom: 1px solid #000000;
|
||||
margin: 0;
|
||||
padding-bottom: 0.2em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.annotationLayer .popup span {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.annotationLayer .popup p {
|
||||
margin: 0;
|
||||
padding-top: 0.2em;
|
||||
border-top: 1px solid #333;
|
||||
margin-top: 2px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.annotationLayer .highlightAnnotation,
|
||||
.annotationLayer .underlineAnnotation,
|
||||
.annotationLayer .squigglyAnnotation,
|
||||
.annotationLayer .strikeoutAnnotation,
|
||||
.annotationLayer .freeTextAnnotation,
|
||||
.annotationLayer .lineAnnotation svg line,
|
||||
.annotationLayer .squareAnnotation svg rect,
|
||||
.annotationLayer .circleAnnotation svg ellipse,
|
||||
.annotationLayer .polylineAnnotation svg polyline,
|
||||
.annotationLayer .polygonAnnotation svg polygon,
|
||||
.annotationLayer .caretAnnotation,
|
||||
.annotationLayer .inkAnnotation svg polyline,
|
||||
.annotationLayer .stampAnnotation,
|
||||
.annotationLayer .fileAttachmentAnnotation {
|
||||
|
@ -279,6 +289,7 @@
|
|||
overflow: visible;
|
||||
border: 9px solid transparent;
|
||||
background-clip: content-box;
|
||||
-webkit-border-image: url(images/shadow.png) 9 9 repeat;
|
||||
-o-border-image: url(images/shadow.png) 9 9 repeat;
|
||||
border-image: url(images/shadow.png) 9 9 repeat;
|
||||
background-color: white;
|
||||
|
@ -543,15 +554,20 @@ select {
|
|||
z-index: 100;
|
||||
border-top: 1px solid #333;
|
||||
|
||||
-webkit-transition-duration: 200ms;
|
||||
|
||||
transition-duration: 200ms;
|
||||
-webkit-transition-timing-function: ease;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
html[dir='ltr'] #sidebarContainer {
|
||||
-webkit-transition-property: left;
|
||||
transition-property: left;
|
||||
left: -200px;
|
||||
left: calc(-1 * var(--sidebar-width));
|
||||
}
|
||||
html[dir='rtl'] #sidebarContainer {
|
||||
-webkit-transition-property: right;
|
||||
transition-property: right;
|
||||
right: -200px;
|
||||
right: calc(-1 * var(--sidebar-width));
|
||||
|
@ -563,6 +579,7 @@ html[dir='rtl'] #sidebarContainer {
|
|||
|
||||
#outerContainer.sidebarResizing #sidebarContainer {
|
||||
/* Improve responsiveness and avoid visual glitches when the sidebar is resized. */
|
||||
-webkit-transition-duration: 0s;
|
||||
transition-duration: 0s;
|
||||
/* Prevent e.g. the thumbnails being selected when the sidebar is resized. */
|
||||
-webkit-user-select: none;
|
||||
|
@ -620,7 +637,9 @@ html[dir='rtl'] #sidebarContent {
|
|||
outline: none;
|
||||
}
|
||||
#viewerContainer:not(.pdfPresentationMode) {
|
||||
-webkit-transition-duration: 200ms;
|
||||
transition-duration: 200ms;
|
||||
-webkit-transition-timing-function: ease;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
html[dir='ltr'] #viewerContainer {
|
||||
|
@ -632,15 +651,18 @@ html[dir='rtl'] #viewerContainer {
|
|||
|
||||
#outerContainer.sidebarResizing #viewerContainer {
|
||||
/* Improve responsiveness and avoid visual glitches when the sidebar is resized. */
|
||||
-webkit-transition-duration: 0s;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
|
||||
html[dir='ltr'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) {
|
||||
-webkit-transition-property: left;
|
||||
transition-property: left;
|
||||
left: 200px;
|
||||
left: var(--sidebar-width);
|
||||
}
|
||||
html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) {
|
||||
-webkit-transition-property: right;
|
||||
transition-property: right;
|
||||
right: 200px;
|
||||
right: var(--sidebar-width);
|
||||
|
@ -662,6 +684,8 @@ html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentatio
|
|||
width: 100%;
|
||||
height: 32px;
|
||||
background-color: #424242; /* fallback */
|
||||
background-image: url(images/texture.png),
|
||||
-webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,30%,.99)), to(hsla(0,0%,25%,.95)));
|
||||
background-image: url(images/texture.png),
|
||||
linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
|
||||
}
|
||||
|
@ -697,6 +721,8 @@ html[dir='rtl'] #sidebarResizer {
|
|||
position: relative;
|
||||
height: 32px;
|
||||
background-color: #474747; /* fallback */
|
||||
background-image: url(images/texture.png),
|
||||
-webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,32%,.99)), to(hsla(0,0%,27%,.95)));
|
||||
background-image: url(images/texture.png),
|
||||
linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
|
||||
}
|
||||
|
@ -733,6 +759,7 @@ html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar {
|
|||
height: 100%;
|
||||
background-color: #ddd;
|
||||
overflow: hidden;
|
||||
-webkit-transition: width 200ms;
|
||||
transition: width 200ms;
|
||||
}
|
||||
|
||||
|
@ -748,6 +775,7 @@ html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar {
|
|||
|
||||
#loadingBar .progress.indeterminate {
|
||||
background-color: #999;
|
||||
-webkit-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
|
@ -815,6 +843,9 @@ html[dir='rtl'] .findbar {
|
|||
#findInput::-webkit-input-placeholder {
|
||||
color: hsl(0, 0%, 75%);
|
||||
}
|
||||
#findInput::-moz-placeholder {
|
||||
font-style: italic;
|
||||
}
|
||||
#findInput:-ms-input-placeholder {
|
||||
font-style: italic;
|
||||
}
|
||||
|
@ -1006,6 +1037,7 @@ html[dir='rtl'] .splitToolbarButton > .toolbarButton {
|
|||
.splitToolbarButton.toggled > .toolbarButton,
|
||||
.toolbarButton.textButton {
|
||||
background-color: hsla(0,0%,0%,.12);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||
background-clip: padding-box;
|
||||
border: 1px solid hsla(0,0%,0%,.35);
|
||||
|
@ -1013,8 +1045,11 @@ html[dir='rtl'] .splitToolbarButton > .toolbarButton {
|
|||
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
||||
0 0 1px hsla(0,0%,100%,.15) inset,
|
||||
0 1px 0 hsla(0,0%,100%,.05);
|
||||
-webkit-transition-property: background-color, border-color, box-shadow;
|
||||
transition-property: background-color, border-color, box-shadow;
|
||||
-webkit-transition-duration: 150ms;
|
||||
transition-duration: 150ms;
|
||||
-webkit-transition-timing-function: ease;
|
||||
transition-timing-function: ease;
|
||||
|
||||
}
|
||||
|
@ -1072,8 +1107,11 @@ html[dir='rtl'] .splitToolbarButtonSeparator {
|
|||
padding: 12px 0;
|
||||
margin: 1px 0;
|
||||
box-shadow: 0 0 0 1px hsla(0,0%,100%,.03);
|
||||
-webkit-transition-property: padding;
|
||||
transition-property: padding;
|
||||
-webkit-transition-duration: 10ms;
|
||||
transition-duration: 10ms;
|
||||
-webkit-transition-timing-function: ease;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
|
@ -1094,8 +1132,11 @@ html[dir='rtl'] .splitToolbarButtonSeparator {
|
|||
user-select: none;
|
||||
/* Opera does not support user-select, use <... unselectable="on"> instead */
|
||||
cursor: default;
|
||||
-webkit-transition-property: background-color, border-color, box-shadow;
|
||||
transition-property: background-color, border-color, box-shadow;
|
||||
-webkit-transition-duration: 150ms;
|
||||
transition-duration: 150ms;
|
||||
-webkit-transition-timing-function: ease;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
|
@ -1117,6 +1158,7 @@ html[dir='rtl'] .dropdownToolbarButton {
|
|||
.secondaryToolbarButton:hover,
|
||||
.secondaryToolbarButton:focus {
|
||||
background-color: hsla(0,0%,0%,.12);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||
background-clip: padding-box;
|
||||
border: 1px solid hsla(0,0%,0%,.35);
|
||||
|
@ -1131,13 +1173,17 @@ html[dir='rtl'] .dropdownToolbarButton {
|
|||
.dropdownToolbarButton:hover:active,
|
||||
.secondaryToolbarButton:hover:active {
|
||||
background-color: hsla(0,0%,0%,.2);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||
border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.4) hsla(0,0%,0%,.45);
|
||||
box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
|
||||
0 0 1px hsla(0,0%,0%,.2) inset,
|
||||
0 1px 0 hsla(0,0%,100%,.05);
|
||||
-webkit-transition-property: background-color, border-color, box-shadow;
|
||||
transition-property: background-color, border-color, box-shadow;
|
||||
-webkit-transition-duration: 10ms;
|
||||
transition-duration: 10ms;
|
||||
-webkit-transition-timing-function: linear;
|
||||
transition-timing-function: linear;
|
||||
}
|
||||
|
||||
|
@ -1145,13 +1191,17 @@ html[dir='rtl'] .dropdownToolbarButton {
|
|||
.splitToolbarButton.toggled > .toolbarButton.toggled,
|
||||
.secondaryToolbarButton.toggled {
|
||||
background-color: hsla(0,0%,0%,.3);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||
border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.45) hsla(0,0%,0%,.5);
|
||||
box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
|
||||
0 0 1px hsla(0,0%,0%,.2) inset,
|
||||
0 1px 0 hsla(0,0%,100%,.05);
|
||||
-webkit-transition-property: background-color, border-color, box-shadow;
|
||||
transition-property: background-color, border-color, box-shadow;
|
||||
-webkit-transition-duration: 10ms;
|
||||
transition-duration: 10ms;
|
||||
-webkit-transition-timing-function: linear;
|
||||
transition-timing-function: linear;
|
||||
}
|
||||
|
||||
|
@ -1493,6 +1543,7 @@ html[dir='rtl'] .verticalToolbarSeparator {
|
|||
border: 1px solid transparent;
|
||||
border-radius: 2px;
|
||||
background-color: hsla(0,0%,100%,.09);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||
background-clip: padding-box;
|
||||
border: 1px solid hsla(0,0%,0%,.35);
|
||||
|
@ -1503,8 +1554,11 @@ html[dir='rtl'] .verticalToolbarSeparator {
|
|||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
outline-style: none;
|
||||
-webkit-transition-property: background-color, border-color, box-shadow;
|
||||
transition-property: background-color, border-color, box-shadow;
|
||||
-webkit-transition-duration: 150ms;
|
||||
transition-duration: 150ms;
|
||||
-webkit-transition-timing-function: ease;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
|
@ -1619,6 +1673,7 @@ a:focus > .thumbnail > .thumbnailSelectionRing > .thumbnailImage,
|
|||
a:focus > .thumbnail > .thumbnailSelectionRing,
|
||||
.thumbnail:hover > .thumbnailSelectionRing {
|
||||
background-color: hsla(0,0%,100%,.15);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||
background-clip: padding-box;
|
||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
||||
|
@ -1634,6 +1689,7 @@ a:focus > .thumbnail > .thumbnailSelectionRing,
|
|||
|
||||
.thumbnail.selected > .thumbnailSelectionRing {
|
||||
background-color: hsla(0,0%,100%,.3);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||
background-clip: padding-box;
|
||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
||||
|
@ -1755,6 +1811,7 @@ html[dir='rtl'] .outlineItemToggler::before {
|
|||
.outlineItem > a:hover,
|
||||
.attachmentsItem > button:hover {
|
||||
background-color: hsla(0,0%,100%,.02);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||
background-clip: padding-box;
|
||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
||||
|
@ -1766,6 +1823,7 @@ html[dir='rtl'] .outlineItemToggler::before {
|
|||
|
||||
.outlineItem.selected {
|
||||
background-color: hsla(0,0%,100%,.08);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0)));
|
||||
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||
background-clip: padding-box;
|
||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
|
||||
|
@ -1850,6 +1908,8 @@ html[dir='rtl'] .outlineItemToggler::before {
|
|||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
background-color: #474747; /* fallback */
|
||||
background-image: url(images/texture.png),
|
||||
-webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,32%,.99)), to(hsla(0,0%,27%,.95)));
|
||||
background-image: url(images/texture.png),
|
||||
linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
|
||||
box-shadow: inset 1px 0 0 hsla(0,0%,100%,.08),
|
||||
|
|
|
@ -26,7 +26,7 @@ html.http-error {
|
|||
|
||||
body{background:#f2f2f2}body h2{font-weight:normal;color:#444}
|
||||
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 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}
|
||||
|
@ -65,6 +65,10 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te
|
|||
.navbar-default .navbar-toggle {border-color: #000;}
|
||||
.cover { margin-bottom: 10px;}
|
||||
.cover-height { max-height: 100px;}
|
||||
.col-sm-2 a .cover-small {
|
||||
margin:5px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.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;}
|
||||
|
@ -78,6 +82,12 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te
|
|||
.spinner {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;}
|
||||
.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)) {
|
||||
var m = (i === 46 || (i >= 48 && i <= 57));
|
||||
if (m !== n) {
|
||||
// Compare has to be with != otherwise fails
|
||||
if (m != n) {
|
||||
tz[++y] = "";
|
||||
n = m;
|
||||
}
|
||||
|
@ -55,7 +56,8 @@ function alphanumCase(a, b) {
|
|||
for (var x = 0; aa[x] && bb[x]; x++) {
|
||||
if (aa[x] !== 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;
|
||||
} else {
|
||||
return (aa[x] > bb[x]) ? 1 : -1;
|
||||
|
|
|
@ -159,10 +159,12 @@ if ( $( 'body.book' ).length > 0 ) {
|
|||
real_custom_column = $( '.real_custom_columns' );
|
||||
// $( '.real_custom_columns' ).remove();
|
||||
$.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("");
|
||||
if (real_cc.length > 1) {
|
||||
$( this ).append( '<span>' + real_cc[0] + '</span><span>' + real_cc[1] + '</span>' );
|
||||
if (real_cc_value != "") {
|
||||
$( this ).append( '<span>' + real_cc_key + '</span><span>' + real_cc_value + '</span>' );
|
||||
}
|
||||
});
|
||||
//$( '.real_custom_columns:nth-child(3)' ).text(function() {
|
||||
|
|
|
@ -15,13 +15,15 @@
|
|||
* 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
|
||||
* 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 */
|
||||
var dbResults = [];
|
||||
var ggResults = [];
|
||||
var cvResults = [];
|
||||
|
||||
$(function () {
|
||||
var msg = i18nMsg;
|
||||
|
@ -33,6 +35,10 @@ $(function () {
|
|||
var ggSearch = "/books/v1/volumes";
|
||||
var ggDone = false;
|
||||
|
||||
var comicvine = "https://comicvine.gamespot.com";
|
||||
var cvSearch = "/api/search/";
|
||||
var cvDone = false;
|
||||
|
||||
var showFlag = 0;
|
||||
|
||||
var templates = {
|
||||
|
@ -48,16 +54,17 @@ $(function () {
|
|||
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);
|
||||
$("#tags").val(uniqueTags.join(","));
|
||||
$("#rating").data("rating").setValue(Math.round(book.rating));
|
||||
$(".cover img").attr("src", book.cover);
|
||||
$("#cover_url").val(book.cover);
|
||||
$("#pubdate").val(book.publishedDate);
|
||||
$("#publisher").val(book.publisher)
|
||||
if (book.series != undefined) {
|
||||
$("#series").val(book.series)
|
||||
$("#publisher").val(book.publisher);
|
||||
if (typeof book.series !== "undefined") {
|
||||
$("#series").val(book.series);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,16 +79,18 @@ $(function () {
|
|||
}
|
||||
function formatDate (date) {
|
||||
var d = new Date(date),
|
||||
month = '' + (d.getMonth() + 1),
|
||||
day = '' + d.getDate(),
|
||||
month = "" + (d.getMonth() + 1),
|
||||
day = "" + d.getDate(),
|
||||
year = d.getFullYear();
|
||||
|
||||
if (month.length < 2)
|
||||
month = '0' + month;
|
||||
if (day.length < 2)
|
||||
day = '0' + day;
|
||||
if (month.length < 2) {
|
||||
month = "0" + month;
|
||||
}
|
||||
if (day.length < 2) {
|
||||
day = "0" + day;
|
||||
}
|
||||
|
||||
return [year, month, day].join('-');
|
||||
return [year, month, day].join("-");
|
||||
}
|
||||
|
||||
if (ggDone && ggResults.length > 0) {
|
||||
|
@ -116,15 +125,16 @@ $(function () {
|
|||
}
|
||||
if (dbDone && dbResults.length > 0) {
|
||||
dbResults.forEach(function(result) {
|
||||
if (result.series){
|
||||
var series_title = result.series.title
|
||||
var seriesTitle = "";
|
||||
if (result.series) {
|
||||
seriesTitle = result.series.title;
|
||||
}
|
||||
var date_fomers = result.pubdate.split("-")
|
||||
var publishedYear = parseInt(date_fomers[0])
|
||||
var publishedMonth = parseInt(date_fomers[1])
|
||||
var publishedDate = new Date(publishedYear, publishedMonth-1, 1)
|
||||
var dateFomers = result.pubdate.split("-");
|
||||
var publishedYear = parseInt(dateFomers[0]);
|
||||
var publishedMonth = parseInt(dateFomers[1]);
|
||||
var publishedDate = new Date(publishedYear, publishedMonth - 1, 1);
|
||||
|
||||
publishedDate = formatDate(publishedDate)
|
||||
publishedDate = formatDate(publishedDate);
|
||||
|
||||
var book = {
|
||||
id: result.id,
|
||||
|
@ -137,7 +147,7 @@ $(function () {
|
|||
return tag.title.toLowerCase().replace(/,/g, "_");
|
||||
}),
|
||||
rating: result.rating.average || 0,
|
||||
series: series_title || "",
|
||||
series: seriesTitle || "",
|
||||
cover: result.image,
|
||||
url: "https://book.douban.com/subject/" + result.id,
|
||||
source: {
|
||||
|
@ -160,6 +170,52 @@ $(function () {
|
|||
});
|
||||
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) {
|
||||
|
@ -183,7 +239,7 @@ $(function () {
|
|||
}
|
||||
|
||||
function dbSearchBook (title) {
|
||||
apikey="0df993c66c0c636e29ecbb5344252a4a"
|
||||
var apikey = "0df993c66c0c636e29ecbb5344252a4a";
|
||||
$.ajax({
|
||||
url: douban + dbSearch + "?apikey=" + apikey + "&q=" + title + "&fields=all&count=10",
|
||||
type: "GET",
|
||||
|
@ -193,7 +249,7 @@ $(function () {
|
|||
dbResults = data.books;
|
||||
},
|
||||
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() {
|
||||
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) {
|
||||
showFlag = 0;
|
||||
$("#meta-info").text(msg.loading);
|
||||
if (keyword) {
|
||||
dbSearchBook(keyword);
|
||||
ggSearchBook(keyword);
|
||||
cvSearchBook(keyword);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
4
cps/static/js/libs/Sortable.min.js
vendored
4
cps/static/js/libs/Sortable.min.js
vendored
File diff suppressed because one or more lines are too long
5311
cps/static/js/libs/pdf.js
vendored
5311
cps/static/js/libs/pdf.js
vendored
File diff suppressed because it is too large
Load Diff
4723
cps/static/js/libs/pdf.worker.js
vendored
4723
cps/static/js/libs/pdf.worker.js
vendored
File diff suppressed because it is too large
Load Diff
1561
cps/static/js/libs/viewer.js
vendored
1561
cps/static/js/libs/viewer.js
vendored
File diff suppressed because it is too large
Load Diff
|
@ -228,6 +228,41 @@ $(function() {
|
|||
$(this).find(".modal-body").html("...");
|
||||
});
|
||||
|
||||
$("#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() {
|
||||
$(".discover .row").isotope("layout");
|
||||
});
|
||||
|
|
|
@ -29,7 +29,7 @@ function sendData(path) {
|
|||
var maxElements;
|
||||
var tmp = [];
|
||||
|
||||
elements = Sortable.utils.find(sortTrue, "div");
|
||||
elements = $(".list-group-item");
|
||||
maxElements = elements.length;
|
||||
|
||||
var form = document.createElement("form");
|
||||
|
|
|
@ -93,6 +93,116 @@ $(function() {
|
|||
var domainId = $(e.relatedTarget).data("domain-id");
|
||||
$(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 */
|
||||
|
@ -104,3 +214,12 @@ function TableActions (value, row, index) {
|
|||
"</a>"
|
||||
].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 غير موجود.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
|
||||
|
|
|
@ -226,6 +226,10 @@ invalid_file_error=Няспраўны або пашкоджаны файл PDF.
|
|||
missing_file_error=Адсутны файл PDF.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
|
||||
|
|
|
@ -226,6 +226,10 @@ invalid_file_error=Μη έγκυρο ή κατεστραμμένο αρχείο
|
|||
missing_file_error=Λείπει αρχείο PDF.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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
|
||||
# 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[one]=תוצאה {{current}} מתוך {{total}}
|
||||
find_match_count[two]={{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
|
||||
# [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[zero]=יותר מ־{{limit}} תוצאות
|
||||
find_match_count_limit[one]=יותר מתוצאה אחת
|
||||
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_more_info=מידע נוסף
|
||||
|
@ -224,6 +226,10 @@ invalid_file_error=קובץ PDF פגום או לא תקין.
|
|||
missing_file_error=קובץ PDF חסר.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
|
||||
|
|
|
@ -208,6 +208,10 @@ invalid_file_error=अमान्य या भ्रष्ट PDF फ़ाइ
|
|||
missing_file_error=\u0020अनुपस्थित PDF फ़ाइल.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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_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.title=Svojstva dokumenta...
|
||||
|
@ -91,8 +103,15 @@ document_properties_creator=Stvaratelj:
|
|||
document_properties_producer=PDF stvaratelj:
|
||||
document_properties_version=PDF inačica:
|
||||
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_a4=A4
|
||||
document_properties_page_size_name_letter=Pismo
|
||||
document_properties_page_size_name_legal=Pravno
|
||||
# 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.
|
||||
|
@ -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}})
|
||||
# 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=Brzi web pregled:
|
||||
document_properties_linearized_yes=Da
|
||||
document_properties_linearized_no=Ne
|
||||
document_properties_close=Zatvori
|
||||
|
@ -145,6 +165,7 @@ find_next.title=Pronađi iduće javljanje ovog izraza
|
|||
find_next_label=Sljedeće
|
||||
find_highlight=Istankni sve
|
||||
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_bottom=Dosegnut vrh dokumenta, nastavak od vrha
|
||||
# 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
|
||||
# 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[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
|
||||
# [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[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
|
||||
|
||||
# Error panel labels
|
||||
|
@ -192,6 +226,10 @@ invalid_file_error=Kriva ili oštećena PDF datoteka.
|
|||
missing_file_error=Nedostaje PDF datoteka.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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_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.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
|
||||
# the total number of matches in the document.
|
||||
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
|
||||
# [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[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ð
|
||||
|
||||
# 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.
|
||||
missing_file_error = File PDF non disponibile.
|
||||
unexpected_response_error = Risposta imprevista del server
|
||||
annotation_date_string = {{date}}, {{time}}
|
||||
text_annotation_type.alt = [Annotazione: {{type}}]
|
||||
password_label = Inserire la password per aprire questo file PDF.
|
||||
password_invalid = Password non corretta. Riprovare.
|
||||
|
|
|
@ -226,6 +226,10 @@ invalid_file_error=無効または破損した PDF ファイル。
|
|||
missing_file_error=PDF ファイルが見つかりません。
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
document_properties_date_string={{date}}, {{time}}
|
||||
document_properties_creator=გამომშვები:
|
||||
document_properties_producer=PDF გამომშვები:
|
||||
document_properties_version=PDF ვერსია:
|
||||
document_properties_producer=PDF-გამომშვები:
|
||||
document_properties_version=PDF-ვერსია:
|
||||
document_properties_page_count=გვერდების რაოდენობა:
|
||||
document_properties_page_size=გვერდის ზომა:
|
||||
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}})
|
||||
# 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=Fast Web View:
|
||||
document_properties_linearized=სწრაფი შეთვალიერება:
|
||||
document_properties_linearized_yes=დიახ
|
||||
document_properties_linearized_no=არა
|
||||
document_properties_close=დახურვა
|
||||
|
@ -154,7 +154,7 @@ findbar_label=ძიება
|
|||
thumb_page_title=გვერდი {{page}}
|
||||
# LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page
|
||||
# number.
|
||||
thumb_page_canvas=გვერდის ესკიზი {{page}}
|
||||
thumb_page_canvas=გვერდის შეთვალიერება {{page}}
|
||||
|
||||
# Find panel button title and messages
|
||||
find_input.title=ძიება
|
||||
|
@ -221,22 +221,26 @@ page_scale_percent={{scale}}%
|
|||
|
||||
# Loading indicator messages
|
||||
loading_error_indicator=შეცდომა
|
||||
loading_error=შეცდომა, PDF ფაილის ჩატვირთვისას.
|
||||
invalid_file_error=არამართებული ან დაზიანებული PDF ფაილი.
|
||||
missing_file_error=ნაკლული PDF ფაილი.
|
||||
loading_error=შეცდომა, PDF-ფაილის ჩატვირთვისას.
|
||||
invalid_file_error=არამართებული ან დაზიანებული PDF-ფაილი.
|
||||
missing_file_error=ნაკლული PDF-ფაილი.
|
||||
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.
|
||||
# "{{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=[{{type}} შენიშვნა]
|
||||
password_label=შეიყვანეთ პაროლი PDF ფაილის გასახსნელად.
|
||||
password_label=შეიყვანეთ პაროლი PDF-ფაილის გასახსნელად.
|
||||
password_invalid=არასწორი პაროლი. გთხოვთ, სცადოთ ხელახლა.
|
||||
password_ok=კარგი
|
||||
password_cancel=გაუქმება
|
||||
|
||||
printing_not_supported=გაფრთხილება: ამობეჭდვა ამ ბრაუზერში არაა სრულად მხარდაჭერილი.
|
||||
printing_not_ready=გაფრთხილება: PDF სრულად ჩატვირთული არაა, ამობეჭდვის დასაწყებად.
|
||||
web_fonts_disabled=ვებშრიფტები გამორთულია: ჩაშენებული PDF შრიფტების გამოყენება ვერ ხერხდება.
|
||||
document_colors_not_allowed=PDF დოკუმენტებს არ აქვს საკუთარი ფერების გამოყენების ნებართვა: ბრაუზერში გამორთულია “გვერდებისთვის საკუთარი ფერების გამოყენების უფლება”.
|
||||
web_fonts_disabled=ვებშრიფტები გამორთულია: ჩაშენებული 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
|
||||
|
|
|
@ -226,6 +226,10 @@ invalid_file_error=Зақымдалған немесе қате PDF файл.
|
|||
missing_file_error=PDF файлы жоқ.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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}}"
|
||||
# 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=({{pagesCount}} 중 {{pageNumber}})
|
||||
page_of_pages=({{pageNumber}} / {{pagesCount}})
|
||||
|
||||
zoom_out.title=축소
|
||||
zoom_out_label=축소
|
||||
zoom_in.title=확대
|
||||
zoom_in_label=확대
|
||||
zoom.title=크기
|
||||
presentation_mode.title=발표 모드로 전환
|
||||
presentation_mode_label=발표 모드
|
||||
zoom.title=확대/축소
|
||||
presentation_mode.title=프레젠테이션 모드로 전환
|
||||
presentation_mode_label=프레젠테이션 모드
|
||||
open_file.title=파일 열기
|
||||
open_file_label=열기
|
||||
print.title=인쇄
|
||||
print_label=인쇄
|
||||
download.title=다운로드
|
||||
download_label=다운로드
|
||||
bookmark.title=지금 보이는 그대로 (복사하거나 새 창에 열기)
|
||||
bookmark_label=지금 보이는 그대로
|
||||
bookmark.title=현재 뷰 (복사하거나 새 창에 열기)
|
||||
bookmark_label=현재 뷰
|
||||
|
||||
# Secondary toolbar and context menu
|
||||
tools.title=도구
|
||||
|
@ -83,7 +83,7 @@ spread_even_label=짝수 펼쳐짐
|
|||
document_properties.title=문서 속성…
|
||||
document_properties_label=문서 속성…
|
||||
document_properties_file_name=파일 이름:
|
||||
document_properties_file_size=파일 사이즈:
|
||||
document_properties_file_size=파일 크기:
|
||||
# LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}"
|
||||
# will be replaced by the PDF file size in kilobytes, respectively in bytes.
|
||||
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.
|
||||
document_properties_mb={{size_mb}} MB ({{size_b}}바이트)
|
||||
document_properties_title=제목:
|
||||
document_properties_author=저자:
|
||||
document_properties_author=작성자:
|
||||
document_properties_subject=주제:
|
||||
document_properties_keywords=키워드:
|
||||
document_properties_creation_date=생성일:
|
||||
document_properties_modification_date=수정일:
|
||||
document_properties_creation_date=작성 날짜:
|
||||
document_properties_modification_date=수정 날짜:
|
||||
# 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_creator=생성자:
|
||||
document_properties_producer=PDF 생성기:
|
||||
document_properties_creator=작성 프로그램:
|
||||
document_properties_producer=PDF 변환 소프트웨어:
|
||||
document_properties_version=PDF 버전:
|
||||
document_properties_page_count=총 페이지:
|
||||
document_properties_page_count=페이지 수:
|
||||
document_properties_page_size=페이지 크기:
|
||||
document_properties_page_size_unit_inches=in
|
||||
document_properties_page_size_unit_millimeters=mm
|
||||
|
@ -127,7 +127,7 @@ document_properties_linearized_yes=예
|
|||
document_properties_linearized_no=아니오
|
||||
document_properties_close=닫기
|
||||
|
||||
print_progress_message=문서 출력 준비중…
|
||||
print_progress_message=인쇄 문서 준비중…
|
||||
# LOCALIZATION NOTE (print_progress_percent): "{{progress}}" will be replaced by
|
||||
# a numerical per cent value.
|
||||
print_progress_percent={{progress}}%
|
||||
|
@ -151,10 +151,10 @@ findbar_label=검색
|
|||
# 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={{page}}쪽
|
||||
thumb_page_title={{page}} 페이지
|
||||
# LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page
|
||||
# number.
|
||||
thumb_page_canvas={{page}}쪽 미리보기
|
||||
thumb_page_canvas={{page}} 페이지 미리보기
|
||||
|
||||
# Find panel button title and messages
|
||||
find_input.title=찾기
|
||||
|
@ -164,7 +164,7 @@ find_previous_label=이전
|
|||
find_next.title=지정 문자열에 일치하는 다음 부분을 검색
|
||||
find_next_label=다음
|
||||
find_highlight=모두 강조 표시
|
||||
find_match_case_label=대문자/소문자 구별
|
||||
find_match_case_label=대/소문자 구분
|
||||
find_entire_word_label=전체 단어
|
||||
find_reached_top=문서 처음까지 검색하고 끝으로 돌아와 검색했습니다.
|
||||
find_reached_bottom=문서 끝까지 검색하고 앞으로 돌아와 검색했습니다.
|
||||
|
@ -208,12 +208,12 @@ error_stack=스택: {{stack}}
|
|||
error_file=파일: {{file}}
|
||||
# LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number
|
||||
error_line=줄 번호: {{line}}
|
||||
rendering_error=페이지를 렌더링하다 오류가 났습니다.
|
||||
rendering_error=페이지를 렌더링하는 중 오류가 발생했습니다.
|
||||
|
||||
# Predefined zoom values
|
||||
page_scale_width=페이지 너비에 맞춤
|
||||
page_scale_fit=페이지에 맞춤
|
||||
page_scale_auto=알아서 맞춤
|
||||
page_scale_auto=자동 맞춤
|
||||
page_scale_actual=실제 크기에 맞춤
|
||||
# LOCALIZATION NOTE (page_scale_percent): "{{scale}}" will be replaced by a
|
||||
# numerical scale value.
|
||||
|
@ -221,22 +221,26 @@ page_scale_percent={{scale}}%
|
|||
|
||||
# Loading indicator messages
|
||||
loading_error_indicator=오류
|
||||
loading_error=PDF를 읽는 중 오류가 생겼습니다.
|
||||
loading_error=PDF를 로드하는 중 오류가 발생했습니다.
|
||||
invalid_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.
|
||||
# "{{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=[{{type}} 주석]
|
||||
password_label=이 PDF 파일을 열 수 있는 암호를 입력하십시오.
|
||||
password_invalid=잘못된 암호입니다. 다시 시도해 주십시오.
|
||||
password_label=이 PDF 파일을 열 수 있는 비밀번호를 입력하십시오.
|
||||
password_invalid=잘못된 비밀번호입니다. 다시 시도해 주십시오.
|
||||
password_ok=확인
|
||||
password_cancel=취소
|
||||
|
||||
printing_not_supported=경고: 이 브라우저는 인쇄를 완전히 지원하지 않습니다.
|
||||
printing_not_ready=경고: 이 PDF를 인쇄를 할 수 있을 정도로 읽어들이지 못했습니다.
|
||||
web_fonts_disabled=웹 폰트가 꺼져있음: 내장된 PDF 글꼴을 쓸 수 없습니다.
|
||||
document_colors_not_allowed=PDF 문서의 색상을 쓰지 못하게 되어 있음: '웹 페이지 자체 색상 사용 허용'이 브라우저에서 꺼져 있습니다.
|
||||
web_fonts_disabled=웹 폰트가 비활성화됨: 내장된 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
|
||||
|
||||
# Secondary toolbar and context menu
|
||||
tools.title=Strumenti
|
||||
tools_label=Strumenti
|
||||
tools.title=Atressi
|
||||
tools_label=Atressi
|
||||
first_page.title=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.title=Propietæ do documento…
|
||||
document_properties_label=Propietæ do documento…
|
||||
document_properties_file_name=Nomme file:
|
||||
document_properties_file_size=Dimenscion file:
|
||||
document_properties_file_name=Nomme schedaio:
|
||||
document_properties_file_size=Dimenscion schedaio:
|
||||
# LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}"
|
||||
# will be replaced by the PDF file size in kilobytes, respectively in bytes.
|
||||
document_properties_kb={{size_kb}} kB ({{size_b}} byte)
|
||||
|
@ -205,7 +205,7 @@ error_message=Mesaggio: {{message}}
|
|||
# trace.
|
||||
error_stack=Stack: {{stack}}
|
||||
# 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
|
||||
error_line=Linia: {{line}}
|
||||
rendering_error=Gh'é stæto 'n'erô itno rendering da pagina.
|
||||
|
@ -222,8 +222,8 @@ page_scale_percent={{scale}}%
|
|||
# Loading indicator messages
|
||||
loading_error_indicator=Erô
|
||||
loading_error=S'é verificou 'n'erô itno caregamento do PDF.
|
||||
invalid_file_error=O file PDF o l'é no valido ò aroinou.
|
||||
missing_file_error=O file PDF o no gh'é.
|
||||
invalid_file_error=O schedaio PDF o l'é no valido ò aroinou.
|
||||
missing_file_error=O schedaio PDF o no gh'é.
|
||||
unexpected_response_error=Risposta inprevista do-u server
|
||||
|
||||
# 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).
|
||||
# Some common types are e.g.: "Check", "Text", "Comment", "Note"
|
||||
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_ok=Va ben
|
||||
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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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_label=हस्त साधन
|
||||
|
||||
scroll_vertical.title=अनुलंब स्क्रोलिंग वापरा
|
||||
scroll_vertical_label=अनुलंब स्क्रोलिंग
|
||||
scroll_horizontal.title=क्षैतिज स्क्रोलिंग वापरा
|
||||
scroll_horizontal_label=क्षैतिज स्क्रोलिंग
|
||||
|
||||
|
||||
# Document properties dialog box
|
||||
|
@ -95,6 +99,7 @@ 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
|
||||
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}})
|
||||
# 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=बंद करा
|
||||
|
@ -151,8 +157,23 @@ find_next.title=वाकप्रयोगची पुढील घटना
|
|||
find_next_label=पुढील
|
||||
find_highlight=सर्व ठळक करा
|
||||
find_match_case_label=आकार जुळवा
|
||||
find_entire_word_label=संपूर्ण शब्द
|
||||
find_reached_top=दस्तऐवजाच्या शीर्षकास पोहचले, तळपासून पुढे
|
||||
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=वाकप्रयोग आढळले नाही
|
||||
|
||||
# Error panel labels
|
||||
|
|
|
@ -226,6 +226,10 @@ invalid_file_error=Ugyldig eller skadet PDF-fil.
|
|||
missing_file_error=Manglende PDF-fil.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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
|
||||
# the total number of matches in the document.
|
||||
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
|
||||
# [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[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=ਵਾਕ ਨਹੀਂ ਲੱਭਿਆ
|
||||
|
||||
# Error panel labels
|
||||
|
|
|
@ -12,13 +12,20 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Main toolbar buttons (tooltips and alt text for images)
|
||||
previous.title=Poprzednia strona
|
||||
previous_label=Poprzednia
|
||||
next.title=Następna strona
|
||||
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}}
|
||||
# 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}})
|
||||
|
||||
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_label=Bieżąca pozycja
|
||||
|
||||
# Secondary toolbar and context menu
|
||||
tools.title=Narzędzia
|
||||
tools_label=Narzędzia
|
||||
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_label=Przewijanie pionowe
|
||||
scroll_horizontal_label=Przewijanie poziome
|
||||
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_label=Widok dwóch stron
|
||||
|
||||
spread_none_label=Brak kolumn
|
||||
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_even_label=Parzyste po lewej
|
||||
spread_odd_label=Nieparzyste po lewej
|
||||
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_label=Właściwości dokumentu…
|
||||
document_properties_file_name=Nazwa pliku:
|
||||
document_properties_file_size=Rozmiar pliku:
|
||||
document_properties_kb={{size_kb}} KB ({{size_b}} b)
|
||||
document_properties_mb={{size_mb}} MB ({{size_b}} b)
|
||||
# LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_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_author=Autor:
|
||||
document_properties_subject=Temat:
|
||||
document_properties_keywords=Słowa kluczowe:
|
||||
document_properties_creation_date=Data utworzenia:
|
||||
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_creator=Utworzony 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_letter=US Letter
|
||||
document_properties_page_size_name_legal=US Legal
|
||||
document_properties_page_size_dimension_string={{width}} × {{height}} {{unit}} (orientacja {{orientation}})
|
||||
document_properties_page_size_dimension_name_string={{width}} × {{height}} {{unit}} ({{name}}, orientacja {{orientation}})
|
||||
# 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}} (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_yes=tak
|
||||
document_properties_linearized_no=nie
|
||||
document_properties_close=Zamknij
|
||||
|
||||
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_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_notification.title=Przełączanie panelu bocznego (dokument zawiera konspekt/załączniki)
|
||||
toggle_sidebar_label=Przełącz panel boczny
|
||||
|
@ -120,26 +148,40 @@ thumbs_label=Miniaturki
|
|||
findbar.title=Znajdź w dokumencie
|
||||
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}}
|
||||
# LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page
|
||||
# number.
|
||||
thumb_page_canvas=Miniaturka strony {{page}}
|
||||
|
||||
# Find panel button title and messages
|
||||
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_label=Poprzednie
|
||||
find_next.title=Znajdź następne wystąpienie tekstu
|
||||
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_entire_word_label=Całe słowa
|
||||
find_reached_top=Początek dokumentu. Wyszukiwanie od końca.
|
||||
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[one]=Pierwsze z {{total}} trafień
|
||||
find_match_count[two]=Drugie z {{total}} trafień
|
||||
find_match_count[few]={{current}}. z {{total}} trafień
|
||||
find_match_count[many]={{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[zero]=Brak trafień.
|
||||
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_not_found=Nie znaleziono tekstu
|
||||
|
||||
# Error panel labels
|
||||
error_more_info=Więcej informacji
|
||||
error_less_info=Mniej informacji
|
||||
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}})
|
||||
# LOCALIZATION NOTE (error_message): "{{message}}" will be replaced by an
|
||||
# english string describing the error.
|
||||
error_message=Wiadomość: {{message}}
|
||||
# LOCALIZATION NOTE (error_stack): "{{stack}}" will be replaced with a stack
|
||||
# trace.
|
||||
error_stack=Stos: {{stack}}
|
||||
# LOCALIZATION NOTE (error_file): "{{file}}" will be replaced with a filename
|
||||
error_file=Plik: {{file}}
|
||||
# LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number
|
||||
error_line=Wiersz: {{line}}
|
||||
rendering_error=Podczas renderowania strony wystąpił błąd.
|
||||
|
||||
# Predefined zoom values
|
||||
page_scale_width=Szerokość strony
|
||||
page_scale_fit=Dopasowanie strony
|
||||
page_scale_auto=Skala automatyczna
|
||||
page_scale_actual=Rozmiar rzeczywisty
|
||||
# LOCALIZATION NOTE (page_scale_percent): "{{scale}}" will be replaced by a
|
||||
# numerical scale value.
|
||||
page_scale_percent={{scale}}%
|
||||
|
||||
# Loading indicator messages
|
||||
loading_error_indicator=Błąd
|
||||
loading_error=Podczas wczytywania dokumentu PDF wystąpił błąd.
|
||||
invalid_file_error=Nieprawidłowy lub uszkodzony plik PDF.
|
||||
missing_file_error=Brak pliku PDF.
|
||||
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}}]
|
||||
password_label=Wprowadź hasło, aby otworzyć ten dokument PDF.
|
||||
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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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_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.
|
||||
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.
|
||||
web_fonts_disabled=As fontes web estão desativadas: não foi possível usar fontes incorporadas do PDF.
|
||||
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_label=Alternar barra lateral
|
||||
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_label=Anexos
|
||||
thumbs.title=Mostrar miniaturas
|
||||
|
@ -226,6 +226,10 @@ invalid_file_error=Ficheiro PDF inválido ou danificado.
|
|||
missing_file_error=Ficheiro PDF inexistente.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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_label=Proprietățile documentului…
|
||||
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}}"
|
||||
# will be replaced by the PDF file size in kilobytes, respectively in bytes.
|
||||
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_version=Versiune PDF:
|
||||
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_millimeters=mm
|
||||
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_fit=Potrivire la pagină
|
||||
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
|
||||
# numerical scale value.
|
||||
page_scale_percent={{scale}}%
|
||||
|
@ -226,6 +226,10 @@ invalid_file_error=Fișier PDF nevalid sau corupt.
|
|||
missing_file_error=Fișier PDF lipsă.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
|
||||
|
|
|
@ -226,6 +226,10 @@ invalid_file_error=Некорректный или повреждённый PDF-
|
|||
missing_file_error=PDF-файл отсутствует.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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=වාමාවර්තව භ්රමණය
|
||||
|
||||
cursor_hand_tool_label=අත් මෙවලම
|
||||
|
||||
|
||||
|
||||
# Document properties dialog box
|
||||
document_properties.title=ලේඛන වත්කම්...
|
||||
|
@ -83,11 +86,32 @@ document_properties_creator=නිර්මාපක:
|
|||
document_properties_producer=PDF නිශ්පාදක:
|
||||
document_properties_version=PDF නිකුතුව:
|
||||
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=වසන්න
|
||||
|
||||
print_progress_message=ලේඛනය මුද්රණය සඳහා සූදානම් කරමින්…
|
||||
# LOCALIZATION NOTE (print_progress_percent): "{{progress}}" will be replaced by
|
||||
# a numerical per cent value.
|
||||
print_progress_percent={{progress}}%
|
||||
print_progress_close=අවලංගු කරන්න
|
||||
|
||||
# Tooltips and alt text for side panel toolbar buttons
|
||||
|
@ -95,6 +119,7 @@ print_progress_close=අවලංගු කරන්න
|
|||
# tooltips)
|
||||
toggle_sidebar.title=පැති තීරුවට මාරුවන්න
|
||||
toggle_sidebar_label=පැති තීරුවට මාරුවන්න
|
||||
document_outline_label=ලේඛනයේ පිට මායිම
|
||||
attachments.title=ඇමිණුම් පෙන්වන්න
|
||||
attachments_label=ඇමිණුම්
|
||||
thumbs.title=සිඟිති රූ පෙන්වන්න
|
||||
|
@ -111,14 +136,25 @@ thumb_page_title=පිටුව {{page}}
|
|||
thumb_page_canvas=පිටුවෙ සිඟිත රූව {{page}}
|
||||
|
||||
# Find panel button title and messages
|
||||
find_input.title=සොයන්න
|
||||
find_previous.title=මේ වාක්ය ඛණ්ඩය මීට පෙර යෙදුණු ස්ථානය සොයන්න
|
||||
find_previous_label=පෙර:
|
||||
find_next.title=මේ වාක්ය ඛණ්ඩය මීළඟට යෙදෙන ස්ථානය සොයන්න
|
||||
find_next_label=මීළඟ
|
||||
find_highlight=සියල්ල උද්දීපනය
|
||||
find_match_case_label=අකුරු ගළපන්න
|
||||
find_entire_word_label=සම්පූර්ණ වචන
|
||||
find_reached_top=පිටුවේ ඉහළ කෙළවරට ලගාවිය, පහළ සිට ඉදිරියට යමින්
|
||||
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=ඔබ සෙව් වචන හමු නොවීය
|
||||
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.label=Pojdi na zadnjo stran
|
||||
last_page_label=Pojdi na zadnjo stran
|
||||
page_rotate_cw.title=Zavrti v smeri urninega kazalca
|
||||
page_rotate_cw.label=Zavrti v smeri urninega kazalca
|
||||
page_rotate_cw_label=Zavrti v smeri urninega kazalca
|
||||
page_rotate_ccw.title=Zavrti v nasprotni smeri urninega kazalca
|
||||
page_rotate_ccw.label=Zavrti v nasprotni smeri urninega kazalca
|
||||
page_rotate_ccw_label=Zavrti v nasprotni smeri urninega kazalca
|
||||
page_rotate_cw.title=Zavrti v smeri urnega kazalca
|
||||
page_rotate_cw.label=Zavrti v smeri urnega kazalca
|
||||
page_rotate_cw_label=Zavrti v smeri urnega kazalca
|
||||
page_rotate_ccw.title=Zavrti v nasprotni smeri urnega kazalca
|
||||
page_rotate_ccw.label=Zavrti v nasprotni smeri urnega 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_label=Orodje za izbor besedila
|
||||
|
@ -226,6 +226,10 @@ invalid_file_error=Neveljavna ali pokvarjena datoteka PDF.
|
|||
missing_file_error=Ni datoteke PDF.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# 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.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
|
||||
|
|
|
@ -202,6 +202,10 @@ invalid_file_error=చెల్లని లేదా పాడైన PDF ఫై
|
|||
missing_file_error=దొరకని PDF ఫైలు.
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
|
||||
|
|
|
@ -226,6 +226,10 @@ invalid_file_error=ไฟล์ PDF ไม่ถูกต้องหรือ
|
|||
missing_file_error=ไฟล์ PDF หายไป
|
||||
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.
|
||||
# "{{type}}" will be replaced with an annotation type from a list defined in
|
||||
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user