Merge branch 'master' into Develop
# Conflicts: # cps/admin.py # cps/config_sql.py # cps/search.py # cps/templates/admin.html # cps/web.py # setup.cfg # test/Calibre-Web TestSummary_Linux.html
@@ -37,7 +37,7 @@ from .reverseproxy import ReverseProxied
|
||||
from .server import WebServer
|
||||
from .dep_check import dependency_check
|
||||
from .updater import Updater
|
||||
from .babel import babel
|
||||
from .babel import babel, get_locale
|
||||
from . import config_sql
|
||||
from . import cache_buster
|
||||
from . import ub, db
|
||||
@@ -147,7 +147,7 @@ def create_app():
|
||||
web_server.stop(True)
|
||||
sys.exit(7)
|
||||
for res in dependency_check() + dependency_check(True):
|
||||
log.info('*** "{}" version does not fit the requirements. '
|
||||
log.info('*** "{}" version does not meet the requirements. '
|
||||
'Should: {}, Found: {}, please consider installing required version ***'
|
||||
.format(res['name'],
|
||||
res['target'],
|
||||
@@ -164,8 +164,11 @@ def create_app():
|
||||
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
|
||||
|
||||
web_server.init_app(app, config)
|
||||
|
||||
babel.init_app(app)
|
||||
if hasattr(babel, "localeselector"):
|
||||
babel.init_app(app)
|
||||
babel.localeselector(get_locale)
|
||||
else:
|
||||
babel.init_app(app, locale_selector=get_locale)
|
||||
|
||||
from . import services
|
||||
|
||||
|
||||
@@ -81,4 +81,4 @@ def stats():
|
||||
categories = calibre_db.session.query(db.Tags).count()
|
||||
series = calibre_db.session.query(db.Series).count()
|
||||
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=collect_stats(),
|
||||
categorycounter=categories, seriecounter=series, title=_(u"Statistics"), page="stat")
|
||||
categorycounter=categories, seriecounter=series, title=_("Statistics"), page="stat")
|
||||
|
||||
@@ -26,15 +26,16 @@ import base64
|
||||
import json
|
||||
import operator
|
||||
import time
|
||||
import sys
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import time as datetime_time
|
||||
from functools import wraps
|
||||
|
||||
|
||||
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
|
||||
from flask_login import login_required, current_user, logout_user, confirm_login
|
||||
from flask_babel import gettext as _
|
||||
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
|
||||
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
|
||||
from flask import session as flask_session
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
@@ -52,27 +53,28 @@ from .services.worker import WorkerThread
|
||||
from .babel import get_available_translations, get_available_locale, get_user_locale_language
|
||||
from . import debug_info
|
||||
|
||||
|
||||
log = logger.create()
|
||||
|
||||
feature_support = {
|
||||
'ldap': bool(services.ldap),
|
||||
'goodreads': bool(services.goodreads_support),
|
||||
'kobo': bool(services.kobo),
|
||||
'updater': constants.UPDATER_AVAILABLE,
|
||||
'gmail': bool(services.gmail),
|
||||
'scheduler': schedule.use_APScheduler,
|
||||
'gdrive': gdrive_support
|
||||
}
|
||||
'ldap': bool(services.ldap),
|
||||
'goodreads': bool(services.goodreads_support),
|
||||
'kobo': bool(services.kobo),
|
||||
'updater': constants.UPDATER_AVAILABLE,
|
||||
'gmail': bool(services.gmail),
|
||||
'scheduler': schedule.use_APScheduler,
|
||||
'gdrive': gdrive_support
|
||||
}
|
||||
|
||||
try:
|
||||
import rarfile # pylint: disable=unused-import
|
||||
|
||||
feature_support['rar'] = True
|
||||
except (ImportError, SyntaxError):
|
||||
feature_support['rar'] = False
|
||||
|
||||
try:
|
||||
from .oauth_bb import oauth_check, oauthblueprints
|
||||
|
||||
feature_support['oauth'] = True
|
||||
except ImportError as err:
|
||||
log.debug('Cannot import Flask-Dance, login with Oauth will not work: %s', err)
|
||||
@@ -80,7 +82,6 @@ except ImportError as err:
|
||||
oauthblueprints = []
|
||||
oauth_check = {}
|
||||
|
||||
|
||||
admi = Blueprint('admin', __name__)
|
||||
|
||||
|
||||
@@ -107,6 +108,7 @@ def before_request():
|
||||
logout_user()
|
||||
g.constants = constants
|
||||
g.user = current_user
|
||||
g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION','')
|
||||
g.allow_registration = config.config_public_reg
|
||||
g.allow_anonymous = config.config_anonbrowse
|
||||
g.allow_upload = config.config_uploading
|
||||
@@ -137,28 +139,39 @@ def admin_forbidden():
|
||||
@admin_required
|
||||
def shutdown():
|
||||
task = request.get_json().get('parameter', -1)
|
||||
showtext = {}
|
||||
show_text = {}
|
||||
if task in (0, 1): # valid commandos received
|
||||
# close all database connections
|
||||
calibre_db.dispose()
|
||||
ub.dispose()
|
||||
|
||||
if task == 0:
|
||||
showtext['text'] = _(u'Server restarted, please reload page')
|
||||
show_text['text'] = _('Server restarted, please reload page.')
|
||||
else:
|
||||
showtext['text'] = _(u'Performing shutdown of server, please close window')
|
||||
show_text['text'] = _('Performing Server shutdown, please close window.')
|
||||
# stop gevent/tornado server
|
||||
web_server.stop(task == 0)
|
||||
return json.dumps(showtext)
|
||||
return json.dumps(show_text)
|
||||
|
||||
if task == 2:
|
||||
log.warning("reconnecting to calibre database")
|
||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||
showtext['text'] = _(u'Reconnect successful')
|
||||
return json.dumps(showtext)
|
||||
show_text['text'] = _('Success! Database Reconnected')
|
||||
return json.dumps(show_text)
|
||||
|
||||
showtext['text'] = _(u'Unknown command')
|
||||
return json.dumps(showtext), 400
|
||||
show_text['text'] = _('Unknown command')
|
||||
return json.dumps(show_text), 400
|
||||
|
||||
|
||||
@admi.route("/metadata_backup", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def queue_metadata_backup():
|
||||
show_text = {}
|
||||
log.warning("Queuing all books for metadata backup")
|
||||
helper.set_all_metadata_dirty()
|
||||
show_text['text'] = _('Success! Books queued for Metadata Backup')
|
||||
return json.dumps(show_text)
|
||||
|
||||
|
||||
# method is available without login and not protected by CSRF to make it easy reachable, is per default switched off
|
||||
@@ -190,14 +203,14 @@ def update_thumbnails():
|
||||
def admin():
|
||||
version = updater_thread.get_current_version_info()
|
||||
if version is False:
|
||||
commit = _(u'Unknown')
|
||||
commit = _('Unknown')
|
||||
else:
|
||||
if 'datetime' in version:
|
||||
commit = version['datetime']
|
||||
|
||||
tz = timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
|
||||
form_date = datetime.strptime(commit[:19], "%Y-%m-%dT%H:%M:%S")
|
||||
if len(commit) > 19: # check if string has timezone
|
||||
if len(commit) > 19: # check if string has timezone
|
||||
if commit[19] == '+':
|
||||
form_date -= timedelta(hours=int(commit[20:22]), minutes=int(commit[23:]))
|
||||
elif commit[19] == '-':
|
||||
@@ -215,7 +228,7 @@ def admin():
|
||||
return render_title_template("admin.html", allUser=all_user, config=config, commit=commit,
|
||||
feature_support=feature_support, schedule_time=schedule_time,
|
||||
schedule_duration=schedule_duration,
|
||||
title=_(u"Admin page"), page="admin")
|
||||
title=_("Admin page"), page="admin")
|
||||
|
||||
|
||||
@admi.route("/admin/dbconfig", methods=["GET", "POST"])
|
||||
@@ -235,7 +248,7 @@ def configuration():
|
||||
config=config,
|
||||
provider=oauthblueprints,
|
||||
feature_support=feature_support,
|
||||
title=_(u"Basic Configuration"), page="config")
|
||||
title=_("Basic Configuration"), page="config")
|
||||
|
||||
|
||||
@admi.route("/admin/ajaxconfig", methods=["POST"])
|
||||
@@ -263,9 +276,9 @@ def calibreweb_alive():
|
||||
@login_required
|
||||
@admin_required
|
||||
def view_configuration():
|
||||
read_column = calibre_db.session.query(db.CustomColumns)\
|
||||
read_column = calibre_db.session.query(db.CustomColumns) \
|
||||
.filter(and_(db.CustomColumns.datatype == 'bool', db.CustomColumns.mark_for_delete == 0)).all()
|
||||
restrict_columns = calibre_db.session.query(db.CustomColumns)\
|
||||
restrict_columns = calibre_db.session.query(db.CustomColumns) \
|
||||
.filter(and_(db.CustomColumns.datatype == 'text', db.CustomColumns.mark_for_delete == 0)).all()
|
||||
languages = calibre_db.speaking_language()
|
||||
translations = get_available_locale()
|
||||
@@ -273,7 +286,7 @@ def view_configuration():
|
||||
restrictColumns=restrict_columns,
|
||||
languages=languages,
|
||||
translations=translations,
|
||||
title=_(u"UI Configuration"), page="uiconfig")
|
||||
title=_("UI Configuration"), page="uiconfig")
|
||||
|
||||
|
||||
@admi.route("/admin/usertable")
|
||||
@@ -284,11 +297,11 @@ def edit_user_table():
|
||||
languages = calibre_db.speaking_language()
|
||||
translations = get_available_locale()
|
||||
all_user = ub.session.query(ub.User)
|
||||
tags = calibre_db.session.query(db.Tags)\
|
||||
.join(db.books_tags_link)\
|
||||
.join(db.Books)\
|
||||
tags = calibre_db.session.query(db.Tags) \
|
||||
.join(db.books_tags_link) \
|
||||
.join(db.Books) \
|
||||
.filter(calibre_db.common_filters()) \
|
||||
.group_by(text('books_tags_link.tag'))\
|
||||
.group_by(text('books_tags_link.tag')) \
|
||||
.order_by(db.Tags.name).all()
|
||||
if config.config_restricted_column:
|
||||
custom_values = calibre_db.session.query(db.cc_classes[config.config_restricted_column]).all()
|
||||
@@ -307,7 +320,7 @@ def edit_user_table():
|
||||
all_roles=constants.ALL_ROLES,
|
||||
kobo_support=kobo_support,
|
||||
sidebar_settings=constants.sidebar_settings,
|
||||
title=_(u"Edit Users"),
|
||||
title=_("Edit Users"),
|
||||
page="usertable")
|
||||
|
||||
|
||||
@@ -465,20 +478,20 @@ def edit_list_user(param):
|
||||
elif param.endswith('role'):
|
||||
value = int(vals['field_index'])
|
||||
if user.name == "Guest" and value in \
|
||||
[constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]:
|
||||
[constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]:
|
||||
raise Exception(_("Guest can't have this role"))
|
||||
# check for valid value, last on checks for power of 2 value
|
||||
if value > 0 and value <= constants.ROLE_VIEWER and (value & value-1 == 0 or value == 1):
|
||||
if value > 0 and value <= constants.ROLE_VIEWER and (value & value - 1 == 0 or value == 1):
|
||||
if vals['value'] == 'true':
|
||||
user.role |= value
|
||||
elif vals['value'] == 'false':
|
||||
if value == constants.ROLE_ADMIN:
|
||||
if not ub.session.query(ub.User).\
|
||||
filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
|
||||
ub.User.id != user.id).count():
|
||||
if not ub.session.query(ub.User). \
|
||||
filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
|
||||
ub.User.id != user.id).count():
|
||||
return Response(
|
||||
json.dumps([{'type': "danger",
|
||||
'message': _(u"No admin user remaining, can't remove admin role",
|
||||
'message': _("No admin user remaining, can't remove admin role",
|
||||
nick=user.name)}]), mimetype='application/json')
|
||||
user.role &= ~value
|
||||
else:
|
||||
@@ -490,7 +503,7 @@ def edit_list_user(param):
|
||||
if user.name == "Guest" and value == constants.SIDEBAR_READ_AND_UNREAD:
|
||||
raise Exception(_("Guest can't have this view"))
|
||||
# check for valid value, last on checks for power of 2 value
|
||||
if value > 0 and value <= constants.SIDEBAR_LIST and (value & value-1 == 0 or value == 1):
|
||||
if value > 0 and value <= constants.SIDEBAR_LIST and (value & value - 1 == 0 or value == 1):
|
||||
if vals['value'] == 'true':
|
||||
user.sidebar_view |= value
|
||||
elif vals['value'] == 'false':
|
||||
@@ -555,13 +568,13 @@ def update_view_configuration():
|
||||
calibre_db.update_title_sort(config)
|
||||
|
||||
if not check_valid_read_column(to_save.get("config_read_column", "0")):
|
||||
flash(_(u"Invalid Read Column"), category="error")
|
||||
flash(_("Invalid Read Column"), category="error")
|
||||
log.debug("Invalid Read column")
|
||||
return view_configuration()
|
||||
_config_int(to_save, "config_read_column")
|
||||
|
||||
if not check_valid_restricted_column(to_save.get("config_restricted_column", "0")):
|
||||
flash(_(u"Invalid Restricted Column"), category="error")
|
||||
flash(_("Invalid Restricted Column"), category="error")
|
||||
log.debug("Invalid Restricted Column")
|
||||
return view_configuration()
|
||||
_config_int(to_save, "config_restricted_column")
|
||||
@@ -581,7 +594,7 @@ def update_view_configuration():
|
||||
config.config_default_show |= constants.DETAIL_RANDOM
|
||||
|
||||
config.save()
|
||||
flash(_(u"Calibre-Web configuration updated"), category="success")
|
||||
flash(_("Calibre-Web configuration updated"), category="success")
|
||||
log.debug("Calibre-Web configuration updated")
|
||||
before_request()
|
||||
|
||||
@@ -643,7 +656,7 @@ def edit_domain(allow):
|
||||
@admin_required
|
||||
def add_domain(allow):
|
||||
domain_name = request.form.to_dict()['domainname'].replace('*', '%').replace('?', '_').lower()
|
||||
check = ub.session.query(ub.Registration).filter(ub.Registration.domain == domain_name)\
|
||||
check = ub.session.query(ub.Registration).filter(ub.Registration.domain == domain_name) \
|
||||
.filter(ub.Registration.allow == allow).first()
|
||||
if not check:
|
||||
new_domain = ub.Registration(domain=domain_name, allow=allow)
|
||||
@@ -861,16 +874,16 @@ def delete_restriction(res_type, user_id):
|
||||
@login_required
|
||||
@admin_required
|
||||
def list_restriction(res_type, user_id):
|
||||
if res_type == 0: # Tags as template
|
||||
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)}
|
||||
if res_type == 0: # Tags as template
|
||||
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd' + str(i)}
|
||||
for i, x in enumerate(config.list_denied_tags()) if x != '']
|
||||
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a'+str(i)}
|
||||
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a' + str(i)}
|
||||
for i, x in enumerate(config.list_allowed_tags()) if x != '']
|
||||
json_dumps = restrict + allow
|
||||
elif res_type == 1: # CustomC as template
|
||||
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)}
|
||||
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)}
|
||||
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a' + str(i)}
|
||||
for i, x in enumerate(config.list_allowed_column_values()) if x != '']
|
||||
json_dumps = restrict + allow
|
||||
elif res_type == 2: # Tags per user
|
||||
@@ -878,9 +891,9 @@ def list_restriction(res_type, user_id):
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
|
||||
else:
|
||||
usr = current_user
|
||||
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)}
|
||||
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)}
|
||||
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a' + str(i)}
|
||||
for i, x in enumerate(usr.list_allowed_tags()) if x != '']
|
||||
json_dumps = restrict + allow
|
||||
elif res_type == 3: # CustomC per user
|
||||
@@ -888,9 +901,9 @@ def list_restriction(res_type, user_id):
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
|
||||
else:
|
||||
usr = current_user
|
||||
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)}
|
||||
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)}
|
||||
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:
|
||||
@@ -920,7 +933,7 @@ def ajax_pathchooser():
|
||||
def check_valid_read_column(column):
|
||||
if column != "0":
|
||||
if not calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.id == column) \
|
||||
.filter(and_(db.CustomColumns.datatype == 'bool', db.CustomColumns.mark_for_delete == 0)).all():
|
||||
.filter(and_(db.CustomColumns.datatype == 'bool', db.CustomColumns.mark_for_delete == 0)).all():
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -928,7 +941,7 @@ def check_valid_read_column(column):
|
||||
def check_valid_restricted_column(column):
|
||||
if column != "0":
|
||||
if not calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.id == column) \
|
||||
.filter(and_(db.CustomColumns.datatype == 'text', db.CustomColumns.mark_for_delete == 0)).all():
|
||||
.filter(and_(db.CustomColumns.datatype == 'text', db.CustomColumns.mark_for_delete == 0)).all():
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -956,7 +969,7 @@ def prepare_tags(user, action, tags_name, id_list):
|
||||
raise Exception(_("Tag not found"))
|
||||
new_tags_list = [x.name for x in tags]
|
||||
else:
|
||||
tags = calibre_db.session.query(db.cc_classes[config.config_restricted_column])\
|
||||
tags = calibre_db.session.query(db.cc_classes[config.config_restricted_column]) \
|
||||
.filter(db.cc_classes[config.config_restricted_column].id.in_(id_list)).all()
|
||||
new_tags_list = [x.value for x in tags]
|
||||
saved_tags_list = user.__dict__[tags_name].split(",") if len(user.__dict__[tags_name]) else []
|
||||
@@ -969,6 +982,19 @@ def prepare_tags(user, action, tags_name, id_list):
|
||||
return ",".join(saved_tags_list)
|
||||
|
||||
|
||||
def get_drives(current):
|
||||
drive_letters = []
|
||||
for d in string.ascii_uppercase:
|
||||
if os.path.exists('{}:'.format(d)) and current[0].lower() != d.lower():
|
||||
drive = "{}:\\".format(d)
|
||||
data = {"name": drive, "fullpath": drive}
|
||||
data["sort"] = "_" + data["fullpath"].lower()
|
||||
data["type"] = "dir"
|
||||
data["size"] = ""
|
||||
drive_letters.append(data)
|
||||
return drive_letters
|
||||
|
||||
|
||||
def pathchooser():
|
||||
browse_for = "folder"
|
||||
folder_only = request.args.get('folder', False) == "true"
|
||||
@@ -976,40 +1002,41 @@ def pathchooser():
|
||||
path = os.path.normpath(request.args.get('path', ""))
|
||||
|
||||
if os.path.isfile(path):
|
||||
oldfile = path
|
||||
old_file = path
|
||||
path = os.path.dirname(path)
|
||||
else:
|
||||
oldfile = ""
|
||||
old_file = ""
|
||||
|
||||
absolute = False
|
||||
|
||||
if os.path.isdir(path):
|
||||
# if os.path.isabs(path):
|
||||
cwd = os.path.realpath(path)
|
||||
absolute = True
|
||||
# else:
|
||||
# cwd = os.path.relpath(path)
|
||||
else:
|
||||
cwd = os.getcwd()
|
||||
|
||||
cwd = os.path.normpath(os.path.realpath(cwd))
|
||||
parentdir = os.path.dirname(cwd)
|
||||
parent_dir = os.path.dirname(cwd)
|
||||
if not absolute:
|
||||
if os.path.realpath(cwd) == os.path.realpath("/"):
|
||||
cwd = os.path.relpath(cwd)
|
||||
else:
|
||||
cwd = os.path.relpath(cwd) + os.path.sep
|
||||
parentdir = os.path.relpath(parentdir) + os.path.sep
|
||||
parent_dir = os.path.relpath(parent_dir) + os.path.sep
|
||||
|
||||
if os.path.realpath(cwd) == os.path.realpath("/"):
|
||||
parentdir = ""
|
||||
files = []
|
||||
if os.path.realpath(cwd) == os.path.realpath("/") \
|
||||
or (sys.platform == "win32" and os.path.realpath(cwd)[1:] == os.path.realpath("/")[1:]):
|
||||
# we are in root
|
||||
parent_dir = ""
|
||||
if sys.platform == "win32":
|
||||
files = get_drives(cwd)
|
||||
|
||||
try:
|
||||
folders = os.listdir(cwd)
|
||||
except Exception:
|
||||
folders = []
|
||||
|
||||
files = []
|
||||
for f in folders:
|
||||
try:
|
||||
data = {"name": f, "fullpath": os.path.join(cwd, f)}
|
||||
@@ -1042,9 +1069,9 @@ def pathchooser():
|
||||
context = {
|
||||
"cwd": cwd,
|
||||
"files": files,
|
||||
"parentdir": parentdir,
|
||||
"parentdir": parent_dir,
|
||||
"type": browse_for,
|
||||
"oldfile": oldfile,
|
||||
"oldfile": old_file,
|
||||
"absolute": absolute,
|
||||
}
|
||||
return json.dumps(context)
|
||||
@@ -1082,10 +1109,10 @@ def _configuration_gdrive_helper(to_save):
|
||||
if not gdrive_secrets:
|
||||
return _configuration_result(_('client_secrets.json Is Not Configured For Web Application'))
|
||||
gdriveutils.update_settings(
|
||||
gdrive_secrets['client_id'],
|
||||
gdrive_secrets['client_secret'],
|
||||
gdrive_secrets['redirect_uris'][0]
|
||||
)
|
||||
gdrive_secrets['client_id'],
|
||||
gdrive_secrets['client_secret'],
|
||||
gdrive_secrets['redirect_uris'][0]
|
||||
)
|
||||
|
||||
# always show Google Drive settings, but in case of error deny support
|
||||
new_gdrive_value = (not gdrive_error) and ("config_use_google_drive" in to_save)
|
||||
@@ -1102,12 +1129,12 @@ def _configuration_oauth_helper(to_save):
|
||||
reboot_required = False
|
||||
for element in oauthblueprints:
|
||||
if to_save["config_" + str(element['id']) + "_oauth_client_id"] != element['oauth_client_id'] \
|
||||
or to_save["config_" + str(element['id']) + "_oauth_client_secret"] != element['oauth_client_secret']:
|
||||
or to_save["config_" + str(element['id']) + "_oauth_client_secret"] != element['oauth_client_secret']:
|
||||
reboot_required = True
|
||||
element['oauth_client_id'] = to_save["config_" + str(element['id']) + "_oauth_client_id"]
|
||||
element['oauth_client_secret'] = to_save["config_" + str(element['id']) + "_oauth_client_secret"]
|
||||
if to_save["config_" + str(element['id']) + "_oauth_client_id"] \
|
||||
and to_save["config_" + str(element['id']) + "_oauth_client_secret"]:
|
||||
and to_save["config_" + str(element['id']) + "_oauth_client_secret"]:
|
||||
active_oauths += 1
|
||||
element["active"] = 1
|
||||
else:
|
||||
@@ -1160,7 +1187,7 @@ def _configuration_ldap_helper(to_save):
|
||||
if not config.config_ldap_provider_url \
|
||||
or not config.config_ldap_port \
|
||||
or not config.config_ldap_dn \
|
||||
or not config.config_ldap_user_object:
|
||||
or not config.config_ldap_user_object:
|
||||
return reboot_required, _configuration_result(_('Please Enter a LDAP Provider, '
|
||||
'Port, DN and User Object Identifier'))
|
||||
|
||||
@@ -1230,7 +1257,7 @@ def new_user():
|
||||
content.default_language = config.config_default_language
|
||||
return render_title_template("user_edit.html", new_user=1, content=content,
|
||||
config=config, translations=translations,
|
||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||
languages=languages, title=_("Add New User"), page="newuser",
|
||||
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||
|
||||
|
||||
@@ -1239,7 +1266,7 @@ def new_user():
|
||||
@admin_required
|
||||
def edit_mailsettings():
|
||||
content = config.get_mail_settings()
|
||||
return render_title_template("email_edit.html", content=content, title=_(u"Edit E-mail Server Settings"),
|
||||
return render_title_template("email_edit.html", content=content, title=_("Edit Email Server Settings"),
|
||||
page="mailset", feature_support=feature_support)
|
||||
|
||||
|
||||
@@ -1258,7 +1285,7 @@ def update_mailsettings():
|
||||
elif to_save.get("gmail"):
|
||||
try:
|
||||
config.mail_gmail_token = services.gmail.setup_gmail(config.mail_gmail_token)
|
||||
flash(_(u"Gmail Account Verification Successful"), category="success")
|
||||
flash(_("Success! Gmail Account Verified."), category="success")
|
||||
except Exception as ex:
|
||||
flash(str(ex), category="error")
|
||||
log.error(ex)
|
||||
@@ -1268,34 +1295,33 @@ def update_mailsettings():
|
||||
_config_int(to_save, "mail_port")
|
||||
_config_int(to_save, "mail_use_ssl")
|
||||
_config_string(to_save, "mail_password_e")
|
||||
_config_int(to_save, "mail_size", lambda y: int(y)*1024*1024)
|
||||
_config_string(to_save, "mail_server")
|
||||
_config_string(to_save, "mail_from")
|
||||
_config_string(to_save, "mail_login")
|
||||
|
||||
_config_int(to_save, "mail_size", lambda y: int(y) * 1024 * 1024)
|
||||
config.mail_server = to_save.get('mail_server', "").strip()
|
||||
config.mail_from = to_save.get('mail_from', "").strip()
|
||||
config.mail_login = to_save.get('mail_login', "").strip()
|
||||
try:
|
||||
config.save()
|
||||
except (OperationalError, InvalidRequestError) as e:
|
||||
ub.session.rollback()
|
||||
log.error_or_exception("Settings Database error: {}".format(e))
|
||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||
return edit_mailsettings()
|
||||
except Exception as e:
|
||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||
return edit_mailsettings()
|
||||
|
||||
if to_save.get("test"):
|
||||
if current_user.email:
|
||||
result = send_test_mail(current_user.email, current_user.name)
|
||||
if result is None:
|
||||
flash(_(u"Test e-mail queued for sending to %(email)s, please check Tasks for result",
|
||||
flash(_("Test e-mail queued for sending to %(email)s, please check Tasks for result",
|
||||
email=current_user.email), category="info")
|
||||
else:
|
||||
flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error")
|
||||
flash(_("There was an error sending the Test e-mail: %(res)s", res=result), category="error")
|
||||
else:
|
||||
flash(_(u"Please configure your e-mail address first..."), category="error")
|
||||
flash(_("Please configure your e-mail address first..."), category="error")
|
||||
else:
|
||||
flash(_(u"E-mail server settings updated"), category="success")
|
||||
flash(_("Email Server Settings updated"), category="success")
|
||||
|
||||
return edit_mailsettings()
|
||||
|
||||
@@ -1309,16 +1335,16 @@ def edit_scheduledtasks():
|
||||
duration_field = list()
|
||||
|
||||
for n in range(24):
|
||||
time_field.append((n, format_time(datetime_time(hour=n), format="short",)))
|
||||
time_field.append((n, format_time(datetime_time(hour=n), format="short", )))
|
||||
for n in range(5, 65, 5):
|
||||
t = timedelta(hours=n // 60, minutes=n % 60)
|
||||
duration_field.append((n, format_timedelta(t, threshold=.9)))
|
||||
duration_field.append((n, format_timedelta(t, threshold=.97)))
|
||||
|
||||
return render_title_template("schedule_edit.html",
|
||||
config=content,
|
||||
starttime=time_field,
|
||||
duration=duration_field,
|
||||
title=_(u"Edit Scheduled Tasks Settings"))
|
||||
title=_("Edit Scheduled Tasks Settings"))
|
||||
|
||||
|
||||
@admi.route("/admin/scheduledtasks", methods=["POST"])
|
||||
@@ -1330,12 +1356,12 @@ def update_scheduledtasks():
|
||||
if 0 <= int(to_save.get("schedule_start_time")) <= 23:
|
||||
_config_int( to_save, "schedule_start_time")
|
||||
else:
|
||||
flash(_(u"Invalid start time for task specified"), category="error")
|
||||
flash(_("Invalid start time for task specified"), category="error")
|
||||
error = True
|
||||
if 0 < int(to_save.get("schedule_duration")) <= 60:
|
||||
_config_int(to_save, "schedule_duration")
|
||||
else:
|
||||
flash(_(u"Invalid duration for task specified"), category="error")
|
||||
flash(_("Invalid duration for task specified"), category="error")
|
||||
error = True
|
||||
_config_checkbox(to_save, "schedule_generate_book_covers")
|
||||
_config_checkbox(to_save, "schedule_generate_series_covers")
|
||||
@@ -1344,7 +1370,7 @@ def update_scheduledtasks():
|
||||
if not error:
|
||||
try:
|
||||
config.save()
|
||||
flash(_(u"Scheduled tasks settings updated"), category="success")
|
||||
flash(_("Scheduled tasks settings updated"), category="success")
|
||||
|
||||
# Cancel any running tasks
|
||||
schedule.end_scheduled_tasks()
|
||||
@@ -1354,7 +1380,7 @@ def update_scheduledtasks():
|
||||
except IntegrityError:
|
||||
ub.session.rollback()
|
||||
log.error("An unknown error occurred while saving scheduled tasks settings")
|
||||
flash(_(u"An unknown error occurred. Please try again later."), category="error")
|
||||
flash(_("Oops! An unknown error occurred. Please try again later."), category="error")
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
log.error("Settings DB is not Writeable")
|
||||
@@ -1369,7 +1395,7 @@ def update_scheduledtasks():
|
||||
def edit_user(user_id):
|
||||
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
|
||||
if not content or (not config.config_anonbrowse and content.name == "Guest"):
|
||||
flash(_(u"User not found"), category="error")
|
||||
flash(_("User not found"), category="error")
|
||||
return redirect(url_for('admin.admin'))
|
||||
languages = calibre_db.speaking_language(return_all_languages=True)
|
||||
translations = get_available_locale()
|
||||
@@ -1388,7 +1414,7 @@ def edit_user(user_id):
|
||||
registered_oauth=oauth_check,
|
||||
mail_configured=config.get_mail_server_configured(),
|
||||
kobo_support=kobo_support,
|
||||
title=_(u"Edit User %(nick)s", nick=content.name),
|
||||
title=_("Edit User %(nick)s", nick=content.name),
|
||||
page="edituser")
|
||||
|
||||
|
||||
@@ -1399,14 +1425,14 @@ def reset_user_password(user_id):
|
||||
if current_user is not None and current_user.is_authenticated:
|
||||
ret, message = reset_password(user_id)
|
||||
if ret == 1:
|
||||
log.debug(u"Password for user %s reset", message)
|
||||
flash(_(u"Password for user %(user)s reset", user=message), category="success")
|
||||
log.debug("Password for user %s reset", message)
|
||||
flash(_("Success! Password for user %(user)s reset", user=message), category="success")
|
||||
elif ret == 0:
|
||||
log.error(u"An unknown error occurred. Please try again later.")
|
||||
flash(_(u"An unknown error occurred. Please try again later."), category="error")
|
||||
log.error("An unknown error occurred. Please try again later.")
|
||||
flash(_("Oops! An unknown error occurred. Please try again later."), category="error")
|
||||
else:
|
||||
log.error(u"Please configure the SMTP mail settings first...")
|
||||
flash(_(u"Please configure the SMTP mail settings first..."), category="error")
|
||||
log.error("Please configure the SMTP mail settings.")
|
||||
flash(_("Oops! Please configure the SMTP mail settings."), category="error")
|
||||
return redirect(url_for('admin.admin'))
|
||||
|
||||
|
||||
@@ -1417,7 +1443,7 @@ def view_logfile():
|
||||
logfiles = {0: logger.get_logfile(config.config_logfile),
|
||||
1: logger.get_accesslogfile(config.config_access_logfile)}
|
||||
return render_title_template("logviewer.html",
|
||||
title=_(u"Logfile viewer"),
|
||||
title=_("Logfile viewer"),
|
||||
accesslog_enable=config.config_access_log,
|
||||
log_enable=bool(config.config_logfile != logger.LOG_TO_STDOUT),
|
||||
logfiles=logfiles,
|
||||
@@ -1467,7 +1493,7 @@ def download_debug():
|
||||
@admin_required
|
||||
def get_update_status():
|
||||
if feature_support['updater']:
|
||||
log.info(u"Update status requested")
|
||||
log.info("Update status requested")
|
||||
return updater_thread.get_available_updates(request.method)
|
||||
else:
|
||||
return ''
|
||||
@@ -1560,7 +1586,7 @@ def ldap_import_create_user(user, user_data):
|
||||
ub.session.add(content)
|
||||
try:
|
||||
ub.session.commit()
|
||||
return 1, None # increase no of users
|
||||
return 1, None # increase no of users
|
||||
except Exception as ex:
|
||||
log.warning("Failed to create LDAP user: %s - %s", user, ex)
|
||||
ub.session.rollback()
|
||||
@@ -1662,7 +1688,7 @@ def _db_configuration_update_helper():
|
||||
except (OperationalError, InvalidRequestError) as e:
|
||||
ub.session.rollback()
|
||||
log.error_or_exception("Settings Database error: {}".format(e))
|
||||
_db_configuration_result(_(u"Database error: %(error)s.", error=e.orig), gdrive_error)
|
||||
_db_configuration_result(_("Oops! Database Error: %(error)s.", error=e.orig), gdrive_error)
|
||||
try:
|
||||
metadata_db = os.path.join(to_save['config_calibre_dir'], "metadata.db")
|
||||
if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db):
|
||||
@@ -1672,7 +1698,7 @@ def _db_configuration_update_helper():
|
||||
return _db_configuration_result('{}'.format(ex), gdrive_error)
|
||||
|
||||
if db_change or not db_valid or not config.db_configured \
|
||||
or config.config_calibre_dir != to_save["config_calibre_dir"]:
|
||||
or config.config_calibre_dir != to_save["config_calibre_dir"]:
|
||||
if not os.path.exists(metadata_db) or not to_save['config_calibre_dir']:
|
||||
return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdrive_error)
|
||||
else:
|
||||
@@ -1694,7 +1720,7 @@ def _db_configuration_update_helper():
|
||||
_config_string(to_save, "config_calibre_dir")
|
||||
calibre_db.update_config(config)
|
||||
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
|
||||
flash(_(u"DB is not Writeable"), category="warning")
|
||||
flash(_("DB is not Writeable"), category="warning")
|
||||
config.save()
|
||||
return _db_configuration_result(None, gdrive_error)
|
||||
|
||||
@@ -1791,7 +1817,7 @@ def _configuration_update_helper():
|
||||
except (OperationalError, InvalidRequestError) as e:
|
||||
ub.session.rollback()
|
||||
log.error_or_exception("Settings Database error: {}".format(e))
|
||||
_configuration_result(_(u"Database error: %(error)s.", error=e.orig))
|
||||
_configuration_result(_("Oops! Database Error: %(error)s.", error=e.orig))
|
||||
|
||||
config.save()
|
||||
if reboot_required:
|
||||
@@ -1807,7 +1833,7 @@ def _configuration_result(error_flash=None, reboot=False):
|
||||
config.load()
|
||||
resp['result'] = [{'type': "danger", 'message': error_flash}]
|
||||
else:
|
||||
resp['result'] = [{'type': "success", 'message': _(u"Calibre-Web configuration updated")}]
|
||||
resp['result'] = [{'type': "success", 'message': _("Calibre-Web configuration updated")}]
|
||||
resp['reboot'] = reboot
|
||||
resp['config_upload'] = config.config_upload_formats
|
||||
return Response(json.dumps(resp), mimetype='application/json')
|
||||
@@ -1838,7 +1864,7 @@ def _db_configuration_result(error_flash=None, gdrive_error=None):
|
||||
gdriveError=gdrive_error,
|
||||
gdrivefolders=gdrivefolders,
|
||||
feature_support=feature_support,
|
||||
title=_(u"Database Configuration"), page="dbconfig")
|
||||
title=_("Database Configuration"), page="dbconfig")
|
||||
|
||||
|
||||
def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
||||
@@ -1853,7 +1879,7 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
||||
try:
|
||||
if not to_save["name"] or not to_save["email"] or not to_save["password"]:
|
||||
log.info("Missing entries on new user")
|
||||
raise Exception(_(u"Please fill out all fields!"))
|
||||
raise Exception(_("Oops! Please complete all fields."))
|
||||
content.password = generate_password_hash(helper.valid_password(to_save.get("password", "")))
|
||||
content.email = check_email(to_save["email"])
|
||||
# Query username, if not existing, change
|
||||
@@ -1862,13 +1888,13 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
||||
content.kindle_mail = valid_email(to_save["kindle_mail"])
|
||||
if config.config_public_reg and not check_valid_domain(content.email):
|
||||
log.info("E-mail: {} for new user is not from valid domain".format(content.email))
|
||||
raise Exception(_(u"E-mail is not from valid domain"))
|
||||
raise Exception(_("E-mail is not from valid domain"))
|
||||
except Exception as ex:
|
||||
flash(str(ex), category="error")
|
||||
return render_title_template("user_edit.html", new_user=1, content=content,
|
||||
config=config,
|
||||
translations=translations,
|
||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||
languages=languages, title=_("Add new user"), page="newuser",
|
||||
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||
try:
|
||||
content.allowed_tags = config.config_allowed_tags
|
||||
@@ -1879,17 +1905,17 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
||||
content.kobo_only_shelves_sync = to_save.get("kobo_only_shelves_sync", 0) == "on"
|
||||
ub.session.add(content)
|
||||
ub.session.commit()
|
||||
flash(_(u"User '%(user)s' created", user=content.name), category="success")
|
||||
flash(_("User '%(user)s' created", user=content.name), category="success")
|
||||
log.debug("User {} created".format(content.name))
|
||||
return redirect(url_for('admin.admin'))
|
||||
except IntegrityError:
|
||||
ub.session.rollback()
|
||||
log.error("Found an existing account for {} or {}".format(content.name, content.email))
|
||||
flash(_("Found an existing account for this e-mail address or name."), category="error")
|
||||
flash(_("Oops! An account already exists for this Email. or name."), category="error")
|
||||
except OperationalError as e:
|
||||
ub.session.rollback()
|
||||
log.error_or_exception("Settings Database error: {}".format(e))
|
||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||
|
||||
|
||||
def _delete_user(content):
|
||||
@@ -1971,10 +1997,11 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
||||
if to_save.get("locale"):
|
||||
content.locale = to_save["locale"]
|
||||
try:
|
||||
if to_save.get('password', "") != "":
|
||||
content.password = generate_password_hash(helper.valid_password(to_save['password']))
|
||||
if to_save.get("email", content.email) != content.email:
|
||||
content.email = check_email(to_save["email"])
|
||||
new_email = valid_email(to_save.get("email", content.email))
|
||||
if not new_email:
|
||||
raise Exception(_("Email can't be empty and has to be a valid Email"))
|
||||
if new_email != content.email:
|
||||
content.email = check_email(new_email)
|
||||
# Query username, if not existing, change
|
||||
if to_save.get("name", content.name) != content.name:
|
||||
if to_save.get("name") == "Guest":
|
||||
@@ -1994,19 +2021,19 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
||||
content=content,
|
||||
config=config,
|
||||
registered_oauth=oauth_check,
|
||||
title=_(u"Edit User %(nick)s", nick=content.name),
|
||||
title=_("Edit User %(nick)s", nick=content.name),
|
||||
page="edituser")
|
||||
try:
|
||||
ub.session_commit()
|
||||
flash(_(u"User '%(nick)s' updated", nick=content.name), category="success")
|
||||
flash(_("User '%(nick)s' updated", nick=content.name), category="success")
|
||||
except IntegrityError as ex:
|
||||
ub.session.rollback()
|
||||
log.error("An unknown error occurred while changing user: {}".format(str(ex)))
|
||||
flash(_(u"An unknown error occurred. Please try again later."), category="error")
|
||||
flash(_("Oops! An unknown error occurred. Please try again later."), category="error")
|
||||
except OperationalError as e:
|
||||
ub.session.rollback()
|
||||
log.error_or_exception("Settings Database error: {}".format(e))
|
||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ log = logger.create()
|
||||
|
||||
babel = Babel()
|
||||
|
||||
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
# if a user is logged in, use the locale from the user settings
|
||||
user = getattr(g, 'user', None)
|
||||
|
||||
@@ -29,7 +29,7 @@ from .constants import DEFAULT_SETTINGS_FILE, DEFAULT_GDRIVE_FILE
|
||||
|
||||
def version_info():
|
||||
if _NIGHTLY_VERSION[1].startswith('$Format'):
|
||||
return "Calibre-Web version: %s - unkown git-clone" % _STABLE_VERSION['version']
|
||||
return "Calibre-Web version: %s - unknown git-clone" % _STABLE_VERSION['version']
|
||||
return "Calibre-Web version: %s -%s" % (_STABLE_VERSION['version'], _NIGHTLY_VERSION[1])
|
||||
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
|
||||
file_path=tmp_file_path,
|
||||
extension=original_file_extension,
|
||||
title=original_file_name,
|
||||
author=u'Unknown',
|
||||
author='Unknown',
|
||||
cover=_extract_cover(tmp_file_path, original_file_extension, rar_executable),
|
||||
description="",
|
||||
tags="",
|
||||
|
||||
@@ -74,12 +74,12 @@ class _Settings(_Base):
|
||||
config_certfile = Column(String)
|
||||
config_keyfile = Column(String)
|
||||
config_trustedhosts = Column(String, default='')
|
||||
config_calibre_web_title = Column(String, default=u'Calibre-Web')
|
||||
config_calibre_web_title = Column(String, default='Calibre-Web')
|
||||
config_books_per_page = Column(Integer, default=60)
|
||||
config_random_books = Column(Integer, default=4)
|
||||
config_authors_max = Column(Integer, default=0)
|
||||
config_read_column = Column(Integer, default=0)
|
||||
config_title_regex = Column(String, default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
|
||||
config_title_regex = Column(String, default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines|Le|La|Les|L\'|Un|Une)\s+')
|
||||
config_theme = Column(Integer, default=0)
|
||||
|
||||
config_log_level = Column(SmallInteger, default=logger.DEFAULT_LOG_LEVEL)
|
||||
|
||||
@@ -163,7 +163,7 @@ def selected_roles(dictionary):
|
||||
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
||||
'series_id, languages, publisher, pubdate, identifiers')
|
||||
|
||||
STABLE_VERSION = {'version': '0.6.19 Beta'}
|
||||
STABLE_VERSION = {'version': '0.6.19'}
|
||||
|
||||
NIGHTLY_VERSION = dict()
|
||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||
|
||||
@@ -111,66 +111,70 @@ class Identifiers(Base):
|
||||
def format_type(self):
|
||||
format_type = self.type.lower()
|
||||
if format_type == 'amazon':
|
||||
return u"Amazon"
|
||||
return "Amazon"
|
||||
elif format_type.startswith("amazon_"):
|
||||
return u"Amazon.{0}".format(format_type[7:])
|
||||
return "Amazon.{0}".format(format_type[7:])
|
||||
elif format_type == "isbn":
|
||||
return u"ISBN"
|
||||
return "ISBN"
|
||||
elif format_type == "doi":
|
||||
return u"DOI"
|
||||
return "DOI"
|
||||
elif format_type == "douban":
|
||||
return u"Douban"
|
||||
return "Douban"
|
||||
elif format_type == "goodreads":
|
||||
return u"Goodreads"
|
||||
return "Goodreads"
|
||||
elif format_type == "babelio":
|
||||
return u"Babelio"
|
||||
return "Babelio"
|
||||
elif format_type == "google":
|
||||
return u"Google Books"
|
||||
return "Google Books"
|
||||
elif format_type == "kobo":
|
||||
return u"Kobo"
|
||||
return "Kobo"
|
||||
elif format_type == "litres":
|
||||
return u"ЛитРес"
|
||||
return "ЛитРес"
|
||||
elif format_type == "issn":
|
||||
return u"ISSN"
|
||||
return "ISSN"
|
||||
elif format_type == "isfdb":
|
||||
return u"ISFDB"
|
||||
return "ISFDB"
|
||||
if format_type == "lubimyczytac":
|
||||
return u"Lubimyczytac"
|
||||
return "Lubimyczytac"
|
||||
if format_type == "databazeknih":
|
||||
return "Databáze knih"
|
||||
else:
|
||||
return self.type
|
||||
|
||||
def __repr__(self):
|
||||
format_type = self.type.lower()
|
||||
if format_type == "amazon" or format_type == "asin":
|
||||
return u"https://amazon.com/dp/{0}".format(self.val)
|
||||
return "https://amazon.com/dp/{0}".format(self.val)
|
||||
elif format_type.startswith('amazon_'):
|
||||
return u"https://amazon.{0}/dp/{1}".format(format_type[7:], self.val)
|
||||
return "https://amazon.{0}/dp/{1}".format(format_type[7:], self.val)
|
||||
elif format_type == "isbn":
|
||||
return u"https://www.worldcat.org/isbn/{0}".format(self.val)
|
||||
return "https://www.worldcat.org/isbn/{0}".format(self.val)
|
||||
elif format_type == "doi":
|
||||
return u"https://dx.doi.org/{0}".format(self.val)
|
||||
return "https://dx.doi.org/{0}".format(self.val)
|
||||
elif format_type == "goodreads":
|
||||
return u"https://www.goodreads.com/book/show/{0}".format(self.val)
|
||||
return "https://www.goodreads.com/book/show/{0}".format(self.val)
|
||||
elif format_type == "babelio":
|
||||
return u"https://www.babelio.com/livres/titre/{0}".format(self.val)
|
||||
return "https://www.babelio.com/livres/titre/{0}".format(self.val)
|
||||
elif format_type == "douban":
|
||||
return u"https://book.douban.com/subject/{0}".format(self.val)
|
||||
return "https://book.douban.com/subject/{0}".format(self.val)
|
||||
elif format_type == "google":
|
||||
return u"https://books.google.com/books?id={0}".format(self.val)
|
||||
return "https://books.google.com/books?id={0}".format(self.val)
|
||||
elif format_type == "kobo":
|
||||
return u"https://www.kobo.com/ebook/{0}".format(self.val)
|
||||
return "https://www.kobo.com/ebook/{0}".format(self.val)
|
||||
elif format_type == "lubimyczytac":
|
||||
return u"https://lubimyczytac.pl/ksiazka/{0}/ksiazka".format(self.val)
|
||||
return "https://lubimyczytac.pl/ksiazka/{0}/ksiazka".format(self.val)
|
||||
elif format_type == "litres":
|
||||
return u"https://www.litres.ru/{0}".format(self.val)
|
||||
return "https://www.litres.ru/{0}".format(self.val)
|
||||
elif format_type == "issn":
|
||||
return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val)
|
||||
return "https://portal.issn.org/resource/ISSN/{0}".format(self.val)
|
||||
elif format_type == "isfdb":
|
||||
return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
|
||||
return "http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
|
||||
elif format_type == "databazeknih":
|
||||
return "https://www.databazeknih.cz/knihy/{0}".format(self.val)
|
||||
elif self.val.lower().startswith("javascript:"):
|
||||
return quote(self.val)
|
||||
else:
|
||||
return u"{0}".format(self.val)
|
||||
return "{0}".format(self.val)
|
||||
|
||||
|
||||
class Comments(Base):
|
||||
@@ -188,7 +192,7 @@ class Comments(Base):
|
||||
return self.text
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Comments({0})>".format(self.text)
|
||||
return "<Comments({0})>".format(self.text)
|
||||
|
||||
|
||||
class Tags(Base):
|
||||
@@ -204,7 +208,7 @@ class Tags(Base):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Tags('{0})>".format(self.name)
|
||||
return "<Tags('{0})>".format(self.name)
|
||||
|
||||
|
||||
class Authors(Base):
|
||||
@@ -224,7 +228,7 @@ class Authors(Base):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link)
|
||||
return "<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link)
|
||||
|
||||
|
||||
class Series(Base):
|
||||
@@ -242,7 +246,7 @@ class Series(Base):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Series('{0},{1}')>".format(self.name, self.sort)
|
||||
return "<Series('{0},{1}')>".format(self.name, self.sort)
|
||||
|
||||
|
||||
class Ratings(Base):
|
||||
@@ -258,7 +262,7 @@ class Ratings(Base):
|
||||
return self.rating
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Ratings('{0}')>".format(self.rating)
|
||||
return "<Ratings('{0}')>".format(self.rating)
|
||||
|
||||
|
||||
class Languages(Base):
|
||||
@@ -277,7 +281,7 @@ class Languages(Base):
|
||||
return self.lang_code
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Languages('{0}')>".format(self.lang_code)
|
||||
return "<Languages('{0}')>".format(self.lang_code)
|
||||
|
||||
|
||||
class Publishers(Base):
|
||||
@@ -295,7 +299,7 @@ class Publishers(Base):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Publishers('{0},{1}')>".format(self.name, self.sort)
|
||||
return "<Publishers('{0},{1}')>".format(self.name, self.sort)
|
||||
|
||||
|
||||
class Data(Base):
|
||||
@@ -319,7 +323,16 @@ class Data(Base):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name)
|
||||
return "<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name)
|
||||
|
||||
|
||||
class Metadata_Dirtied(Base):
|
||||
__tablename__ = 'metadata_dirtied'
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
book = Column(Integer, ForeignKey('books.id'), nullable=False, unique=True)
|
||||
|
||||
def __init__(self, book):
|
||||
self.book = book
|
||||
|
||||
|
||||
class Books(Base):
|
||||
@@ -364,7 +377,7 @@ class Books(Base):
|
||||
self.has_cover = (has_cover != None)
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort,
|
||||
return "<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort,
|
||||
self.timestamp, self.pubdate, self.series_index,
|
||||
self.last_modified, self.path, self.has_cover)
|
||||
|
||||
@@ -390,6 +403,30 @@ class CustomColumns(Base):
|
||||
display_dict = json.loads(self.display)
|
||||
return display_dict
|
||||
|
||||
def to_json(self, value, extra, sequence):
|
||||
content = dict()
|
||||
content['table'] = "custom_column_" + str(self.id)
|
||||
content['column'] = "value"
|
||||
content['datatype'] = self.datatype
|
||||
content['is_multiple'] = None if not self.is_multiple else self.is_multiple
|
||||
content['kind'] = "field"
|
||||
content['name'] = self.name
|
||||
content['search_terms'] = ['#' + self.label]
|
||||
content['label'] = self.label
|
||||
content['colnum'] = self.id
|
||||
content['display'] = self.get_display_dict()
|
||||
content['is_custom'] = True
|
||||
content['is_category'] = self.datatype in ['text', 'rating', 'enumeration', 'series']
|
||||
content['link_column'] = "value"
|
||||
content['category_sort'] = "value"
|
||||
content['is_csp'] = False
|
||||
content['is_editable'] = self.editable
|
||||
content['rec_index'] = sequence + 22 # toDo why ??
|
||||
content['#value#'] = value
|
||||
content['#extra#'] = extra
|
||||
content['is_multiple2'] = {}
|
||||
return json.dumps(content, ensure_ascii=False)
|
||||
|
||||
|
||||
class AlchemyEncoder(json.JSONEncoder):
|
||||
|
||||
@@ -641,6 +678,18 @@ class CalibreDB:
|
||||
def get_book_format(self, book_id, file_format):
|
||||
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first()
|
||||
|
||||
def set_metadata_dirty(self, book_id):
|
||||
if not self.session.query(Metadata_Dirtied).filter(Metadata_Dirtied.book == book_id).one_or_none():
|
||||
self.session.add(Metadata_Dirtied(book_id))
|
||||
|
||||
def delete_dirty_metadata(self, book_id):
|
||||
try:
|
||||
self.session.query(Metadata_Dirtied).filter(Metadata_Dirtied.book == book_id).delete()
|
||||
self.session.commit()
|
||||
except (OperationalError) as e:
|
||||
self.session.rollback()
|
||||
log.error("Database error: {}".format(e))
|
||||
|
||||
# Language and content filters for displaying in the UI
|
||||
def common_filters(self, allow_show_archived=False, return_all_languages=False):
|
||||
if not allow_show_archived:
|
||||
|
||||
@@ -38,7 +38,7 @@ from flask_babel import gettext as _
|
||||
from flask_babel import lazy_gettext as N_
|
||||
from flask_babel import get_locale
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy.exc import OperationalError, IntegrityError
|
||||
from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError
|
||||
from sqlalchemy.orm.exc import StaleDataError
|
||||
|
||||
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status
|
||||
@@ -107,7 +107,7 @@ def edit_book(book_id):
|
||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||
# Book not found
|
||||
if not book:
|
||||
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
|
||||
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
|
||||
category="error")
|
||||
return redirect(url_for("web.index"))
|
||||
|
||||
@@ -151,7 +151,7 @@ def edit_book(book_id):
|
||||
if to_save.get("cover_url", None):
|
||||
if not current_user.role_upload():
|
||||
edit_error = True
|
||||
flash(_(u"User has no rights to upload cover"), category="error")
|
||||
flash(_("User has no rights to upload cover"), category="error")
|
||||
if to_save["cover_url"].endswith('/static/generic_cover.jpg'):
|
||||
book.has_cover = 0
|
||||
else:
|
||||
@@ -203,6 +203,7 @@ def edit_book(book_id):
|
||||
if modify_date:
|
||||
book.last_modified = datetime.utcnow()
|
||||
kobo_sync_status.remove_synced_book(edited_books_id, all=True)
|
||||
calibre_db.set_metadata_dirty(book.id)
|
||||
|
||||
calibre_db.session.merge(book)
|
||||
calibre_db.session.commit()
|
||||
@@ -222,10 +223,10 @@ def edit_book(book_id):
|
||||
calibre_db.session.rollback()
|
||||
flash(str(e), category="error")
|
||||
return redirect(url_for('web.show_book', book_id=book.id))
|
||||
except (OperationalError, IntegrityError, StaleDataError) as e:
|
||||
except (OperationalError, IntegrityError, StaleDataError, InterfaceError) as e:
|
||||
log.error_or_exception("Database error: {}".format(e))
|
||||
calibre_db.session.rollback()
|
||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||
return redirect(url_for('web.show_book', book_id=book.id))
|
||||
except Exception as ex:
|
||||
log.error_or_exception(ex)
|
||||
@@ -277,6 +278,8 @@ def upload():
|
||||
|
||||
move_coverfile(meta, db_book)
|
||||
|
||||
if modify_date:
|
||||
calibre_db.set_metadata_dirty(book_id)
|
||||
# save data to database, reread data
|
||||
calibre_db.session.commit()
|
||||
|
||||
@@ -285,7 +288,7 @@ def upload():
|
||||
if error:
|
||||
flash(error, category="error")
|
||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(title))
|
||||
upload_text = N_(u"File %(file)s uploaded", file=link)
|
||||
upload_text = N_("File %(file)s uploaded", file=link)
|
||||
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(title)))
|
||||
helper.add_book_to_thumbnail_cache(book_id)
|
||||
|
||||
@@ -299,7 +302,7 @@ def upload():
|
||||
except (OperationalError, IntegrityError, StaleDataError) as e:
|
||||
calibre_db.session.rollback()
|
||||
log.error_or_exception("Database error: {}".format(e))
|
||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||
|
||||
|
||||
@@ -312,7 +315,7 @@ def convert_bookformat(book_id):
|
||||
book_format_to = request.form.get('book_format_to', None)
|
||||
|
||||
if (book_format_from is None) or (book_format_to is None):
|
||||
flash(_(u"Source or destination format for conversion missing"), category="error")
|
||||
flash(_("Source or destination format for conversion missing"), category="error")
|
||||
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
||||
|
||||
log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to)
|
||||
@@ -320,11 +323,11 @@ def convert_bookformat(book_id):
|
||||
book_format_to.upper(), current_user.name)
|
||||
|
||||
if rtn is None:
|
||||
flash(_(u"Book successfully queued for converting to %(book_format)s",
|
||||
flash(_("Book successfully queued for converting to %(book_format)s",
|
||||
book_format=book_format_to),
|
||||
category="success")
|
||||
else:
|
||||
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
|
||||
flash(_("There was an error converting this book: %(res)s", res=rtn), category="error")
|
||||
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
||||
|
||||
|
||||
@@ -555,6 +558,7 @@ def table_xchange_author_title():
|
||||
renamed_author=renamed)
|
||||
if modify_date:
|
||||
book.last_modified = datetime.utcnow()
|
||||
calibre_db.set_metadata_dirty(book.id)
|
||||
try:
|
||||
calibre_db.session.commit()
|
||||
except (OperationalError, IntegrityError, StaleDataError) as e:
|
||||
@@ -569,9 +573,9 @@ def table_xchange_author_title():
|
||||
|
||||
|
||||
def merge_metadata(to_save, meta):
|
||||
if to_save.get('author_name', "") == _(u'Unknown'):
|
||||
if to_save.get('author_name', "") == _('Unknown'):
|
||||
to_save['author_name'] = ''
|
||||
if to_save.get('book_title', "") == _(u'Unknown'):
|
||||
if to_save.get('book_title', "") == _('Unknown'):
|
||||
to_save['book_title'] = ''
|
||||
for s_field, m_field in [
|
||||
('tags', 'tags'), ('author_name', 'author'), ('series', 'series'),
|
||||
@@ -607,7 +611,7 @@ def prepare_authors(authr):
|
||||
|
||||
# we have all author names now
|
||||
if input_authors == ['']:
|
||||
input_authors = [_(u'Unknown')] # prevent empty Author
|
||||
input_authors = [_('Unknown')] # prevent empty Author
|
||||
|
||||
renamed = list()
|
||||
for in_aut in input_authors:
|
||||
@@ -624,11 +628,11 @@ def prepare_authors(authr):
|
||||
|
||||
|
||||
def prepare_authors_on_upload(title, authr):
|
||||
if title != _(u'Unknown') and authr != _(u'Unknown'):
|
||||
if title != _('Unknown') and authr != _('Unknown'):
|
||||
entry = calibre_db.check_exists_book(authr, title)
|
||||
if entry:
|
||||
log.info("Uploaded book probably exists in library")
|
||||
flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ")
|
||||
flash(_("Uploaded book probably exists in the library, consider to change before upload new: ")
|
||||
+ Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning")
|
||||
|
||||
input_authors, renamed = prepare_authors(authr)
|
||||
@@ -683,7 +687,7 @@ def create_book_on_upload(modify_date, meta):
|
||||
modify_date |= edit_book_languages(meta.languages, db_book, upload_mode=True, invalid=invalid)
|
||||
if invalid:
|
||||
for lang in invalid:
|
||||
flash(_(u"'%(langname)s' is not a valid language", langname=lang), category="warning")
|
||||
flash(_("'%(langname)s' is not a valid language", langname=lang), category="warning")
|
||||
|
||||
# handle tags
|
||||
modify_date |= edit_book_tags(meta.tags, db_book)
|
||||
@@ -733,7 +737,7 @@ def file_handling_on_upload(requested_file):
|
||||
meta = uploader.upload(requested_file, config.config_rarfile_location)
|
||||
except (IOError, OSError):
|
||||
log.error("File %s could not saved to temp dir", requested_file.filename)
|
||||
flash(_(u"File %(filename)s could not saved to temp dir",
|
||||
flash(_("File %(filename)s could not saved to temp dir",
|
||||
filename=requested_file.filename), category="error")
|
||||
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||
return meta, None
|
||||
@@ -753,7 +757,7 @@ def move_coverfile(meta, db_book):
|
||||
os.unlink(meta.cover)
|
||||
except OSError as e:
|
||||
log.error("Failed to move cover file %s: %s", new_cover_path, e)
|
||||
flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_cover_path,
|
||||
flash(_("Failed to Move Cover File %(file)s: %(error)s", file=new_cover_path,
|
||||
error=e),
|
||||
category="error")
|
||||
|
||||
@@ -767,7 +771,7 @@ def delete_whole_book(book_id, book):
|
||||
|
||||
# check if only this book links to:
|
||||
# author, language, series, tags, custom columns
|
||||
modify_database_object([u''], book.authors, db.Authors, calibre_db.session, 'author')
|
||||
modify_database_object([''], book.authors, db.Authors, calibre_db.session, 'author')
|
||||
modify_database_object([u''], book.tags, db.Tags, calibre_db.session, 'tags')
|
||||
modify_database_object([u''], book.series, db.Series, calibre_db.session, 'series')
|
||||
modify_database_object([u''], book.languages, db.Languages, calibre_db.session, 'languages')
|
||||
@@ -888,7 +892,7 @@ def render_edit_book(book_id):
|
||||
cc = calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all()
|
||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||
if not book:
|
||||
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
|
||||
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
|
||||
category="error")
|
||||
return redirect(url_for("web.index"))
|
||||
|
||||
@@ -923,7 +927,7 @@ def render_edit_book(book_id):
|
||||
if kepub_possible:
|
||||
allowed_conversion_formats.append('kepub')
|
||||
return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc,
|
||||
title=_(u"edit metadata"), page="editbook",
|
||||
title=_("edit metadata"), page="editbook",
|
||||
conversion_formats=allowed_conversion_formats,
|
||||
config=config,
|
||||
source_formats=valid_source_formats)
|
||||
@@ -1008,7 +1012,7 @@ def edit_book_languages(languages, book, upload_mode=False, invalid=None):
|
||||
if isinstance(invalid, list):
|
||||
invalid.append(lang)
|
||||
else:
|
||||
raise ValueError(_(u"'%(langname)s' is not a valid language", langname=lang))
|
||||
raise ValueError(_("'%(langname)s' is not a valid language", langname=lang))
|
||||
# ToDo: Not working correct
|
||||
if upload_mode and len(input_l) == 1:
|
||||
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view
|
||||
@@ -1150,7 +1154,7 @@ def upload_single_file(file_request, book, book_id):
|
||||
# check for empty request
|
||||
if requested_file.filename != '':
|
||||
if not current_user.role_upload():
|
||||
flash(_(u"User has no rights to upload additional file formats"), category="error")
|
||||
flash(_("User has no rights to upload additional file formats"), category="error")
|
||||
return False
|
||||
if '.' in requested_file.filename:
|
||||
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
|
||||
@@ -1171,12 +1175,12 @@ def upload_single_file(file_request, book, book_id):
|
||||
try:
|
||||
os.makedirs(filepath)
|
||||
except OSError:
|
||||
flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
|
||||
flash(_("Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
|
||||
return False
|
||||
try:
|
||||
requested_file.save(saved_filename)
|
||||
except OSError:
|
||||
flash(_(u"Failed to store file %(file)s.", file=saved_filename), category="error")
|
||||
flash(_("Failed to store file %(file)s.", file=saved_filename), category="error")
|
||||
return False
|
||||
|
||||
file_size = os.path.getsize(saved_filename)
|
||||
@@ -1194,12 +1198,12 @@ def upload_single_file(file_request, book, book_id):
|
||||
except (OperationalError, IntegrityError, StaleDataError) as e:
|
||||
calibre_db.session.rollback()
|
||||
log.error_or_exception("Database error: {}".format(e))
|
||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||
return False # return redirect(url_for('web.show_book', book_id=book.id))
|
||||
|
||||
# Queue uploader info
|
||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title))
|
||||
upload_text = N_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link)
|
||||
upload_text = N_("File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link)
|
||||
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(book.title)))
|
||||
|
||||
return uploader.process(
|
||||
@@ -1214,7 +1218,7 @@ def upload_cover(cover_request, book):
|
||||
# check for empty request
|
||||
if requested_file.filename != '':
|
||||
if not current_user.role_upload():
|
||||
flash(_(u"User has no rights to upload cover"), category="error")
|
||||
flash(_("User has no rights to upload cover"), category="error")
|
||||
return False
|
||||
ret, message = helper.save_cover(requested_file, book.path)
|
||||
if ret is True:
|
||||
|
||||
@@ -80,13 +80,13 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
||||
if epub_metadata['subject'] == 'Unknown':
|
||||
epub_metadata['subject'] = ''
|
||||
|
||||
if epub_metadata['publisher'] == u'Unknown':
|
||||
if epub_metadata['publisher'] == 'Unknown':
|
||||
epub_metadata['publisher'] = ''
|
||||
|
||||
if epub_metadata['date'] == u'Unknown':
|
||||
if epub_metadata['date'] == 'Unknown':
|
||||
epub_metadata['date'] = ''
|
||||
|
||||
if epub_metadata['description'] == u'Unknown':
|
||||
if epub_metadata['description'] == 'Unknown':
|
||||
description = tree.xpath("//*[local-name() = 'description']/text()")
|
||||
if len(description) > 0:
|
||||
epub_metadata['description'] = description
|
||||
@@ -102,11 +102,14 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
||||
|
||||
identifiers = []
|
||||
for node in p.xpath('dc:identifier', namespaces=ns):
|
||||
identifier_name=node.attrib.values()[-1];
|
||||
identifier_value=node.text;
|
||||
if identifier_name in ('uuid','calibre'):
|
||||
continue;
|
||||
identifiers.append( [identifier_name, identifier_value] )
|
||||
try:
|
||||
identifier_name = node.attrib.values()[-1]
|
||||
except IndexError:
|
||||
continue
|
||||
identifier_value = node.text
|
||||
if identifier_name in ('uuid', 'calibre') or identifier_value is None:
|
||||
continue
|
||||
identifiers.append([identifier_name, identifier_value])
|
||||
|
||||
if not epub_metadata['title']:
|
||||
title = original_file_name
|
||||
|
||||
@@ -38,19 +38,19 @@ def get_fb2_info(tmp_file_path, original_file_extension):
|
||||
if len(last_name):
|
||||
last_name = last_name[0]
|
||||
else:
|
||||
last_name = u''
|
||||
last_name = ''
|
||||
middle_name = element.xpath('fb:middle-name/text()', namespaces=ns)
|
||||
if len(middle_name):
|
||||
middle_name = middle_name[0]
|
||||
else:
|
||||
middle_name = u''
|
||||
middle_name = ''
|
||||
first_name = element.xpath('fb:first-name/text()', namespaces=ns)
|
||||
if len(first_name):
|
||||
first_name = first_name[0]
|
||||
else:
|
||||
first_name = u''
|
||||
return (first_name + u' '
|
||||
+ middle_name + u' '
|
||||
first_name = ''
|
||||
return (first_name + ' '
|
||||
+ middle_name + ' '
|
||||
+ last_name)
|
||||
|
||||
author = str(", ".join(map(get_author, authors)))
|
||||
@@ -59,12 +59,12 @@ def get_fb2_info(tmp_file_path, original_file_extension):
|
||||
if len(title):
|
||||
title = str(title[0])
|
||||
else:
|
||||
title = u''
|
||||
title = ''
|
||||
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
|
||||
if len(description):
|
||||
description = str(description[0])
|
||||
else:
|
||||
description = u''
|
||||
description = ''
|
||||
|
||||
return BookMeta(
|
||||
file_path=tmp_file_path,
|
||||
|
||||
@@ -55,7 +55,7 @@ def authenticate_google_drive():
|
||||
try:
|
||||
authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl()
|
||||
except gdriveutils.InvalidConfigError:
|
||||
flash(_(u'Google Drive setup not completed, try to deactivate and activate Google Drive again'),
|
||||
flash(_('Google Drive setup not completed, try to deactivate and activate Google Drive again'),
|
||||
category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
return redirect(authUrl)
|
||||
@@ -91,9 +91,9 @@ def watch_gdrive():
|
||||
config.save()
|
||||
except HttpError as e:
|
||||
reason=json.loads(e.content)['error']['errors'][0]
|
||||
if reason['reason'] == u'push.webhookUrlUnauthorized':
|
||||
flash(_(u'Callback domain is not verified, '
|
||||
u'please follow steps to verify domain in google developer console'), category="error")
|
||||
if reason['reason'] == 'push.webhookUrlUnauthorized':
|
||||
flash(_('Callback domain is not verified, '
|
||||
'please follow steps to verify domain in google developer console'), category="error")
|
||||
else:
|
||||
flash(reason['message'], category="error")
|
||||
|
||||
|
||||
@@ -556,7 +556,7 @@ def updateGdriveCalibreFromLocal():
|
||||
|
||||
# update gdrive.db on edit of books title
|
||||
def updateDatabaseOnEdit(ID,newPath):
|
||||
sqlCheckPath = newPath if newPath[-1] == '/' else newPath + u'/'
|
||||
sqlCheckPath = newPath if newPath[-1] == '/' else newPath + '/'
|
||||
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
|
||||
if storedPathName:
|
||||
storedPathName.path = sqlCheckPath
|
||||
@@ -578,6 +578,7 @@ def deleteDatabaseEntry(ID):
|
||||
|
||||
|
||||
# Gets cover file from gdrive
|
||||
# ToDo: Check is this right everyone get read permissions on cover files?
|
||||
def get_cover_via_gdrive(cover_path):
|
||||
df = getFileFromEbooksFolder(cover_path, 'cover.jpg')
|
||||
if df:
|
||||
@@ -600,6 +601,29 @@ def get_cover_via_gdrive(cover_path):
|
||||
else:
|
||||
return None
|
||||
|
||||
# Gets cover file from gdrive
|
||||
def get_metadata_backup_via_gdrive(metadata_path):
|
||||
df = getFileFromEbooksFolder(metadata_path, 'metadata.opf')
|
||||
if df:
|
||||
if not session.query(PermissionAdded).filter(PermissionAdded.gdrive_id == df['id']).first():
|
||||
df.GetPermissions()
|
||||
df.InsertPermission({
|
||||
'type': 'anyone',
|
||||
'value': 'anyone',
|
||||
'role': 'writer', # ToDo needs write access
|
||||
'withLink': True})
|
||||
permissionAdded = PermissionAdded()
|
||||
permissionAdded.gdrive_id = df['id']
|
||||
session.add(permissionAdded)
|
||||
try:
|
||||
session.commit()
|
||||
except OperationalError as ex:
|
||||
log.error_or_exception('Database error: {}'.format(ex))
|
||||
session.rollback()
|
||||
return df.metadata.get('webContentLink')
|
||||
else:
|
||||
return None
|
||||
|
||||
# Creates chunks for downloading big files
|
||||
def partial(total_byte_len, part_size_limit):
|
||||
s = []
|
||||
|
||||
@@ -31,6 +31,7 @@ import unidecode
|
||||
from flask import send_from_directory, make_response, redirect, abort, url_for
|
||||
from flask_babel import gettext as _
|
||||
from flask_babel import lazy_gettext as N_
|
||||
from flask_babel import get_locale
|
||||
from flask_login import current_user
|
||||
from sqlalchemy.sql.expression import true, false, and_, or_, text, func
|
||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||
@@ -57,6 +58,7 @@ from .subproc_wrapper import process_wait
|
||||
from .services.worker import WorkerThread
|
||||
from .tasks.mail import TaskEmail
|
||||
from .tasks.thumbnail import TaskClearCoverThumbnailCache, TaskGenerateCoverThumbnails
|
||||
from .tasks.metadata_backup import TaskBackupMetadata
|
||||
|
||||
log = logger.create()
|
||||
|
||||
@@ -74,30 +76,30 @@ except (ImportError, RuntimeError) as e:
|
||||
def convert_book_format(book_id, calibre_path, old_book_format, new_book_format, user_id, ereader_mail=None):
|
||||
book = calibre_db.get_book(book_id)
|
||||
data = calibre_db.get_book_format(book.id, old_book_format)
|
||||
file_path = os.path.join(calibre_path, book.path, data.name)
|
||||
if not data:
|
||||
error_message = _(u"%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id)
|
||||
error_message = _("%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id)
|
||||
log.error("convert_book_format: %s", error_message)
|
||||
return error_message
|
||||
file_path = os.path.join(calibre_path, book.path, data.name)
|
||||
if config.config_use_google_drive:
|
||||
if not gd.getFileFromEbooksFolder(book.path, data.name + "." + old_book_format.lower()):
|
||||
error_message = _(u"%(format)s not found on Google Drive: %(fn)s",
|
||||
error_message = _("%(format)s not found on Google Drive: %(fn)s",
|
||||
format=old_book_format, fn=data.name + "." + old_book_format.lower())
|
||||
return error_message
|
||||
else:
|
||||
if not os.path.exists(file_path + "." + old_book_format.lower()):
|
||||
error_message = _(u"%(format)s not found: %(fn)s",
|
||||
error_message = _("%(format)s not found: %(fn)s",
|
||||
format=old_book_format, fn=data.name + "." + old_book_format.lower())
|
||||
return error_message
|
||||
# read settings and append converter task to queue
|
||||
if ereader_mail:
|
||||
settings = config.get_mail_settings()
|
||||
settings['subject'] = _('Send to E-Reader') # pretranslate Subject for e-mail
|
||||
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
|
||||
settings['subject'] = _('Send to eReader') # pretranslate Subject for Email
|
||||
settings['body'] = _('This Email has been sent via Calibre-Web.')
|
||||
else:
|
||||
settings = dict()
|
||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) # prevent xss
|
||||
txt = u"{} -> {}: {}".format(
|
||||
txt = "{} -> {}: {}".format(
|
||||
old_book_format.upper(),
|
||||
new_book_format.upper(),
|
||||
link)
|
||||
@@ -109,30 +111,30 @@ def convert_book_format(book_id, calibre_path, old_book_format, new_book_format,
|
||||
|
||||
# Texts are not lazy translated as they are supposed to get send out as is
|
||||
def send_test_mail(ereader_mail, user_name):
|
||||
WorkerThread.add(user_name, TaskEmail(_(u'Calibre-Web test e-mail'), None, None,
|
||||
config.get_mail_settings(), ereader_mail, N_(u"Test e-mail"),
|
||||
_(u'This e-mail has been sent via Calibre-Web.')))
|
||||
WorkerThread.add(user_name, TaskEmail(_('Calibre-Web Test Email'), None, None,
|
||||
config.get_mail_settings(), ereader_mail, N_("Test Email"),
|
||||
_('This Email has been sent via Calibre-Web.')))
|
||||
return
|
||||
|
||||
|
||||
# Send registration email or password reset email, depending on parameter resend (False means welcome email)
|
||||
def send_registration_mail(e_mail, user_name, default_password, resend=False):
|
||||
txt = "Hello %s!\r\n" % user_name
|
||||
txt = "Hi %s!\r\n" % user_name
|
||||
if not resend:
|
||||
txt += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n"
|
||||
txt += "Please log in to your account using the following informations:\r\n"
|
||||
txt += "User name: %s\r\n" % user_name
|
||||
txt += "Your account at Calibre-Web has been created.\r\n"
|
||||
txt += "Please log in using the following information:\r\n"
|
||||
txt += "Username: %s\r\n" % user_name
|
||||
txt += "Password: %s\r\n" % default_password
|
||||
txt += "Don't forget to change your password after first login.\r\n"
|
||||
txt += "Sincerely\r\n\r\n"
|
||||
txt += "Your Calibre-Web team"
|
||||
txt += "Don't forget to change your password after your first login.\r\n"
|
||||
txt += "Regards,\r\n\r\n"
|
||||
txt += "Calibre-Web"
|
||||
WorkerThread.add(None, TaskEmail(
|
||||
subject=_(u'Get Started with Calibre-Web'),
|
||||
subject=_('Get Started with Calibre-Web'),
|
||||
filepath=None,
|
||||
attachment=None,
|
||||
settings=config.get_mail_settings(),
|
||||
recipient=e_mail,
|
||||
task_message=N_(u"Registration e-mail for user: %(name)s", name=user_name),
|
||||
task_message=N_("Registration Email for user: %(name)s", name=user_name),
|
||||
text=txt
|
||||
))
|
||||
return
|
||||
@@ -143,13 +145,13 @@ def check_send_to_ereader_with_converter(formats):
|
||||
if 'MOBI' in formats and 'EPUB' not in formats:
|
||||
book_formats.append({'format': 'Epub',
|
||||
'convert': 1,
|
||||
'text': _('Convert %(orig)s to %(format)s and send to E-Reader',
|
||||
'text': _('Convert %(orig)s to %(format)s and send to eReader',
|
||||
orig='Mobi',
|
||||
format='Epub')})
|
||||
if 'AZW3' in formats and 'EPUB' not in formats:
|
||||
book_formats.append({'format': 'Epub',
|
||||
'convert': 2,
|
||||
'text': _('Convert %(orig)s to %(format)s and send to E-Reader',
|
||||
'text': _('Convert %(orig)s to %(format)s and send to eReader',
|
||||
orig='Azw3',
|
||||
format='Epub')})
|
||||
return book_formats
|
||||
@@ -157,7 +159,7 @@ def check_send_to_ereader_with_converter(formats):
|
||||
|
||||
def check_send_to_ereader(entry):
|
||||
"""
|
||||
returns all available book formats for sending to E-Reader
|
||||
returns all available book formats for sending to eReader
|
||||
"""
|
||||
formats = list()
|
||||
book_formats = list()
|
||||
@@ -168,24 +170,24 @@ def check_send_to_ereader(entry):
|
||||
if 'EPUB' in formats:
|
||||
book_formats.append({'format': 'Epub',
|
||||
'convert': 0,
|
||||
'text': _('Send %(format)s to E-Reader', format='Epub')})
|
||||
'text': _('Send %(format)s to eReader', format='Epub')})
|
||||
if 'MOBI' in formats:
|
||||
book_formats.append({'format': 'Mobi',
|
||||
'convert': 0,
|
||||
'text': _('Send %(format)s to E-Reader', format='Mobi')})
|
||||
'text': _('Send %(format)s to eReader', format='Mobi')})
|
||||
if 'PDF' in formats:
|
||||
book_formats.append({'format': 'Pdf',
|
||||
'convert': 0,
|
||||
'text': _('Send %(format)s to E-Reader', format='Pdf')})
|
||||
'text': _('Send %(format)s to eReader', format='Pdf')})
|
||||
if 'AZW' in formats:
|
||||
book_formats.append({'format': 'Azw',
|
||||
'convert': 0,
|
||||
'text': _('Send %(format)s to E-Reader', format='Azw')})
|
||||
'text': _('Send %(format)s to eReader', format='Azw')})
|
||||
if config.config_converterpath:
|
||||
book_formats.extend(check_send_to_ereader_with_converter(formats))
|
||||
return book_formats
|
||||
else:
|
||||
log.error(u'Cannot find book entry %d', entry.id)
|
||||
log.error('Cannot find book entry %d', entry.id)
|
||||
return None
|
||||
|
||||
|
||||
@@ -202,30 +204,30 @@ def check_read_formats(entry):
|
||||
|
||||
|
||||
# Files are processed in the following order/priority:
|
||||
# 1: If Mobi file is existing, it's directly send to E-Reader email,
|
||||
# 2: If Epub file is existing, it's converted and send to E-Reader email,
|
||||
# 3: If Pdf file is existing, it's directly send to E-Reader email
|
||||
# 1: If Mobi file is existing, it's directly send to eReader email,
|
||||
# 2: If Epub file is existing, it's converted and send to eReader email,
|
||||
# 3: If Pdf file is existing, it's directly send to eReader email
|
||||
def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_id):
|
||||
"""Send email with attachments"""
|
||||
book = calibre_db.get_book(book_id)
|
||||
|
||||
if convert == 1:
|
||||
# returns None if success, otherwise errormessage
|
||||
return convert_book_format(book_id, calibrepath, u'epub', book_format.lower(), user_id, ereader_mail)
|
||||
return convert_book_format(book_id, calibrepath, 'epub', book_format.lower(), user_id, ereader_mail)
|
||||
if convert == 2:
|
||||
# returns None if success, otherwise errormessage
|
||||
return convert_book_format(book_id, calibrepath, u'azw3', book_format.lower(), user_id, ereader_mail)
|
||||
return convert_book_format(book_id, calibrepath, 'azw3', book_format.lower(), user_id, ereader_mail)
|
||||
|
||||
for entry in iter(book.data):
|
||||
if entry.format.upper() == book_format.upper():
|
||||
converted_file_name = entry.name + '.' + book_format.lower()
|
||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(book.title))
|
||||
email_text = N_(u"%(book)s send to E-Reader", book=link)
|
||||
WorkerThread.add(user_id, TaskEmail(_(u"Send to E-Reader"), book.path, converted_file_name,
|
||||
email_text = N_("%(book)s send to eReader", book=link)
|
||||
WorkerThread.add(user_id, TaskEmail(_("Send to eReader"), book.path, converted_file_name,
|
||||
config.get_mail_settings(), ereader_mail,
|
||||
email_text, _(u'This e-mail has been sent via Calibre-Web.')))
|
||||
email_text, _('This Email has been sent via Calibre-Web.')))
|
||||
return
|
||||
return _(u"The requested file could not be read. Maybe wrong permissions?")
|
||||
return _("The requested file could not be read. Maybe wrong permissions?")
|
||||
|
||||
|
||||
def get_valid_filename(value, replace_whitespace=True, chars=128):
|
||||
@@ -233,16 +235,16 @@ def get_valid_filename(value, replace_whitespace=True, chars=128):
|
||||
Returns the given string converted to a string that can be used for a clean
|
||||
filename. Limits num characters to 128 max.
|
||||
"""
|
||||
if value[-1:] == u'.':
|
||||
value = value[:-1]+u'_'
|
||||
if value[-1:] == '.':
|
||||
value = value[:-1]+'_'
|
||||
value = value.replace("/", "_").replace(":", "_").strip('\0')
|
||||
if config.config_unicode_filename:
|
||||
value = (unidecode.unidecode(value))
|
||||
if replace_whitespace:
|
||||
# *+:\"/<>? are replaced by _
|
||||
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
|
||||
value = re.sub(r'[*+:\\\"/<>?]+', '_', value, flags=re.U)
|
||||
# pipe has to be replaced with comma
|
||||
value = re.sub(r'[|]+', u',', value, flags=re.U)
|
||||
value = re.sub(r'[|]+', ',', value, flags=re.U)
|
||||
|
||||
value = value.encode('utf-8')[:chars].decode('utf-8', errors='ignore').strip()
|
||||
|
||||
@@ -339,7 +341,7 @@ def edit_book_read_status(book_id, read_status=None):
|
||||
return "Custom Column No.{} does not exist in calibre database".format(config.config_read_column)
|
||||
except (OperationalError, InvalidRequestError) as ex:
|
||||
calibre_db.session.rollback()
|
||||
log.error(u"Read status could not set: {}".format(ex))
|
||||
log.error("Read status could not set: {}".format(ex))
|
||||
return _("Read status could not set: {}".format(ex.orig))
|
||||
return ""
|
||||
|
||||
@@ -414,8 +416,8 @@ def clean_author_database(renamed_author, calibre_path="", local_book=None, gdri
|
||||
g_file = gd.getFileFromEbooksFolder(all_new_path,
|
||||
file_format.name + '.' + file_format.format.lower())
|
||||
if g_file:
|
||||
gd.moveGdriveFileRemote(g_file, all_new_name + u'.' + file_format.format.lower())
|
||||
gd.updateDatabaseOnEdit(g_file['id'], all_new_name + u'.' + file_format.format.lower())
|
||||
gd.moveGdriveFileRemote(g_file, all_new_name + '.' + file_format.format.lower())
|
||||
gd.updateDatabaseOnEdit(g_file['id'], all_new_name + '.' + file_format.format.lower())
|
||||
else:
|
||||
log.error("File {} not found on gdrive"
|
||||
.format(all_new_path, file_format.name + '.' + file_format.format.lower()))
|
||||
@@ -508,25 +510,25 @@ def update_dir_structure_gdrive(book_id, first_author, renamed_author):
|
||||
authordir = book.path.split('/')[0]
|
||||
titledir = book.path.split('/')[1]
|
||||
new_authordir = rename_all_authors(first_author, renamed_author, gdrive=True)
|
||||
new_titledir = get_valid_filename(book.title, chars=96) + u" (" + str(book_id) + u")"
|
||||
new_titledir = get_valid_filename(book.title, chars=96) + " (" + str(book_id) + ")"
|
||||
|
||||
if titledir != new_titledir:
|
||||
g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
|
||||
if g_file:
|
||||
gd.moveGdriveFileRemote(g_file, new_titledir)
|
||||
book.path = book.path.split('/')[0] + u'/' + new_titledir
|
||||
book.path = book.path.split('/')[0] + '/' + new_titledir
|
||||
gd.updateDatabaseOnEdit(g_file['id'], book.path) # only child folder affected
|
||||
else:
|
||||
return _(u'File %(file)s not found on Google Drive', file=book.path) # file not found
|
||||
return _('File %(file)s not found on Google Drive', file=book.path) # file not found
|
||||
|
||||
if authordir != new_authordir and authordir not in renamed_author:
|
||||
g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), new_titledir)
|
||||
if g_file:
|
||||
gd.moveGdriveFolderRemote(g_file, new_authordir)
|
||||
book.path = new_authordir + u'/' + book.path.split('/')[1]
|
||||
book.path = new_authordir + '/' + book.path.split('/')[1]
|
||||
gd.updateDatabaseOnEdit(g_file['id'], book.path)
|
||||
else:
|
||||
return _(u'File %(file)s not found on Google Drive', file=authordir) # file not found
|
||||
return _('File %(file)s not found on Google Drive', file=authordir) # file not found
|
||||
|
||||
# change location in database to new author/title path
|
||||
book.path = os.path.join(new_authordir, new_titledir).replace('\\', '/')
|
||||
@@ -598,7 +600,7 @@ def delete_book_gdrive(book, book_format):
|
||||
gd.deleteDatabaseEntry(g_file['id'])
|
||||
g_file.Trash()
|
||||
else:
|
||||
error = _(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found
|
||||
error = _('Book path %(path)s not found on Google Drive', path=book.path) # file not found
|
||||
|
||||
return error is None, error
|
||||
|
||||
@@ -638,26 +640,28 @@ def uniq(inpt):
|
||||
def check_email(email):
|
||||
email = valid_email(email)
|
||||
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == email.lower()).first():
|
||||
log.error(u"Found an existing account for this e-mail address")
|
||||
raise Exception(_(u"Found an existing account for this e-mail address"))
|
||||
log.error("Found an existing account for this Email address")
|
||||
raise Exception(_("Found an existing account for this Email address"))
|
||||
return email
|
||||
|
||||
|
||||
def check_username(username):
|
||||
username = username.strip()
|
||||
if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).scalar():
|
||||
log.error(u"This username is already taken")
|
||||
raise Exception(_(u"This username is already taken"))
|
||||
log.error("This username is already taken")
|
||||
raise Exception(_("This username is already taken"))
|
||||
return username
|
||||
|
||||
|
||||
def valid_email(email):
|
||||
email = email.strip()
|
||||
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
|
||||
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
|
||||
email):
|
||||
log.error(u"Invalid e-mail address format")
|
||||
raise Exception(_(u"Invalid e-mail address format"))
|
||||
# if email is not deleted
|
||||
if email:
|
||||
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
|
||||
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
|
||||
email):
|
||||
log.error("Invalid Email address format")
|
||||
raise Exception(_("Invalid Email address format"))
|
||||
return email
|
||||
|
||||
def valid_password(check_password):
|
||||
@@ -699,7 +703,8 @@ def update_dir_structure(book_id,
|
||||
|
||||
def delete_book(book, calibrepath, book_format):
|
||||
if not book_format:
|
||||
clear_cover_thumbnail_cache(book.id) ## here it breaks
|
||||
clear_cover_thumbnail_cache(book.id) ## here it breaks
|
||||
calibre_db.delete_dirty_metadata(book.id)
|
||||
if config.config_use_google_drive:
|
||||
return delete_book_gdrive(book, book_format)
|
||||
else:
|
||||
@@ -849,8 +854,8 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
|
||||
try:
|
||||
os.makedirs(filepath)
|
||||
except OSError:
|
||||
log.error(u"Failed to create path for cover")
|
||||
return False, _(u"Failed to create path for cover")
|
||||
log.error("Failed to create path for cover")
|
||||
return False, _("Failed to create path for cover")
|
||||
try:
|
||||
# upload of jgp file without wand
|
||||
if isinstance(img, requests.Response):
|
||||
@@ -865,8 +870,8 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
|
||||
# upload of jpg/png... from hdd
|
||||
img.save(os.path.join(filepath, saved_filename))
|
||||
except (IOError, OSError):
|
||||
log.error(u"Cover-file is not a valid image file, or could not be stored")
|
||||
return False, _(u"Cover-file is not a valid image file, or could not be stored")
|
||||
log.error("Cover-file is not a valid image file, or could not be stored")
|
||||
return False, _("Cover-file is not a valid image file, or could not be stored")
|
||||
return True, None
|
||||
|
||||
|
||||
@@ -956,7 +961,7 @@ def check_unrar(unrar_location):
|
||||
|
||||
except (OSError, UnicodeDecodeError) as err:
|
||||
log.error_or_exception(err)
|
||||
return _('Error excecuting UnRar')
|
||||
return _('Error executing UnRar')
|
||||
|
||||
|
||||
def json_serial(obj):
|
||||
@@ -1045,3 +1050,11 @@ def add_book_to_thumbnail_cache(book_id):
|
||||
def update_thumbnail_cache():
|
||||
if config.schedule_generate_book_covers:
|
||||
WorkerThread.add(None, TaskGenerateCoverThumbnails())
|
||||
|
||||
|
||||
def set_all_metadata_dirty():
|
||||
WorkerThread.add(None, TaskBackupMetadata(export_language=get_locale(),
|
||||
translated_title=_("Cover"),
|
||||
set_dirty=True,
|
||||
task_message=N_("Queue all books for metadata backup")),
|
||||
hidden=False)
|
||||
|
||||
@@ -45,6 +45,7 @@ import requests
|
||||
|
||||
|
||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
||||
from . import isoLanguages
|
||||
from .constants import sqlalchemy_version2, COVER_THUMBNAIL_SMALL
|
||||
from .helper import get_download_link
|
||||
from .services import SyncToken as SyncToken
|
||||
@@ -155,7 +156,7 @@ def HandleSyncRequest():
|
||||
new_archived_last_modified = datetime.datetime.min
|
||||
sync_results = []
|
||||
|
||||
# We reload the book database so that the user get's a fresh view of the library
|
||||
# We reload the book database so that the user gets a fresh view of the library
|
||||
# in case of external changes (e.g: adding a book through Calibre).
|
||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||
|
||||
@@ -355,7 +356,7 @@ def HandleMetadataRequest(book_uuid):
|
||||
log.info("Kobo library metadata request received for book %s" % book_uuid)
|
||||
book = calibre_db.get_book_by_uuid(book_uuid)
|
||||
if not book or not book.data:
|
||||
log.info(u"Book %s not found in database", book_uuid)
|
||||
log.info("Book %s not found in database", book_uuid)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
metadata = get_metadata(book)
|
||||
@@ -443,6 +444,12 @@ def get_seriesindex(book):
|
||||
return book.series_index or 1
|
||||
|
||||
|
||||
def get_language(book):
|
||||
if not book.languages:
|
||||
return 'en'
|
||||
return isoLanguages.get(part3=book.languages[0].lang_code).part1
|
||||
|
||||
|
||||
def get_metadata(book):
|
||||
download_urls = []
|
||||
kepub = [data for data in book.data if data.format == 'KEPUB']
|
||||
@@ -480,7 +487,7 @@ def get_metadata(book):
|
||||
"IsInternetArchive": False,
|
||||
"IsPreOrder": False,
|
||||
"IsSocialEnabled": True,
|
||||
"Language": "en",
|
||||
"Language": get_language(book),
|
||||
"PhoneticPronunciations": {},
|
||||
"PublicationDate": convert_to_kobo_timestamp_string(book.pubdate),
|
||||
"Publisher": {"Imprint": "", "Name": get_publisher(book), },
|
||||
@@ -508,7 +515,7 @@ def get_metadata(book):
|
||||
@requires_kobo_auth
|
||||
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
||||
def HandleTagCreate():
|
||||
# catch delete requests, otherwise the are handeld in the book delete handler
|
||||
# catch delete requests, otherwise the are handled in the book delete handler
|
||||
if request.method == "DELETE":
|
||||
abort(405)
|
||||
name, items = None, None
|
||||
@@ -752,7 +759,7 @@ def create_kobo_tag(shelf):
|
||||
for book_shelf in shelf.books:
|
||||
book = calibre_db.get_book(book_shelf.book_id)
|
||||
if not book:
|
||||
log.info(u"Book (id: %s) in BookShelf (id: %s) not found in book database", book_shelf.book_id, shelf.id)
|
||||
log.info("Book (id: %s) in BookShelf (id: %s) not found in book database", book_shelf.book_id, shelf.id)
|
||||
continue
|
||||
tag["Items"].append(
|
||||
{
|
||||
@@ -769,7 +776,7 @@ def create_kobo_tag(shelf):
|
||||
def HandleStateRequest(book_uuid):
|
||||
book = calibre_db.get_book_by_uuid(book_uuid)
|
||||
if not book or not book.data:
|
||||
log.info(u"Book %s not found in database", book_uuid)
|
||||
log.info("Book %s not found in database", book_uuid)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
kobo_reading_state = get_or_create_reading_state(book.id)
|
||||
@@ -944,7 +951,7 @@ def HandleBookDeletionRequest(book_uuid):
|
||||
log.info("Kobo book delete request received for book %s" % book_uuid)
|
||||
book = calibre_db.get_book_by_uuid(book_uuid)
|
||||
if not book:
|
||||
log.info(u"Book %s not found in database", book_uuid)
|
||||
log.info("Book %s not found in database", book_uuid)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
book_id = book.id
|
||||
@@ -958,7 +965,7 @@ def HandleBookDeletionRequest(book_uuid):
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
|
||||
def HandleUnimplementedRequest(dummy=None):
|
||||
log.debug("Unimplemented Library Request received: %s", request.base_url)
|
||||
log.debug("Unimplemented Library Request received: %s (request is forwarded to kobo if configured)", request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
|
||||
@@ -970,7 +977,7 @@ def HandleUnimplementedRequest(dummy=None):
|
||||
@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)
|
||||
log.debug("Unimplemented User Request received: %s (request is forwarded to kobo if configured)", request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
|
||||
@@ -1010,7 +1017,7 @@ def handle_getests():
|
||||
@kobo.route("/v1/affiliate", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/deals", methods=["GET", "POST"])
|
||||
def HandleProductsRequest(dummy=None):
|
||||
log.debug("Unimplemented Products Request received: %s", request.base_url)
|
||||
log.debug("Unimplemented Products Request received: %s (request is forwarded to kobo if configured)", request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ def generate_auth_token(user_id):
|
||||
|
||||
return render_title_template(
|
||||
"generate_kobo_auth_url.html",
|
||||
title=_(u"Kobo Setup"),
|
||||
title=_("Kobo Setup"),
|
||||
auth_token=auth_token.auth_token,
|
||||
warning = warning
|
||||
)
|
||||
|
||||
@@ -63,11 +63,11 @@ class Amazon(Metadata):
|
||||
r.raise_for_status()
|
||||
except Exception as ex:
|
||||
log.warning(ex)
|
||||
return
|
||||
return None
|
||||
long_soup = BS(r.text, "lxml") #~4sec :/
|
||||
soup2 = long_soup.find("div", attrs={"cel_widget_id": "dpx-books-ppd_csm_instrumentation_wrapper"})
|
||||
if soup2 is None:
|
||||
return
|
||||
return None
|
||||
try:
|
||||
match = MetaRecord(
|
||||
title = "",
|
||||
@@ -115,7 +115,7 @@ class Amazon(Metadata):
|
||||
return match, index
|
||||
except Exception as e:
|
||||
log.error_or_exception(e)
|
||||
return
|
||||
return None
|
||||
|
||||
val = list()
|
||||
if self.active:
|
||||
@@ -127,10 +127,10 @@ class Amazon(Metadata):
|
||||
results.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
log.error_or_exception(e)
|
||||
return None
|
||||
return []
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
return None
|
||||
return []
|
||||
soup = BS(results.text, 'html.parser')
|
||||
links_list = [next(filter(lambda i: "digital-text" in i["href"], x.findAll("a")))["href"] for x in
|
||||
soup.findAll("div", attrs={"data-component-type": "s-search-result"})]
|
||||
|
||||
@@ -43,7 +43,8 @@ class Douban(Metadata):
|
||||
__id__ = "douban"
|
||||
DESCRIPTION = "豆瓣"
|
||||
META_URL = "https://book.douban.com/"
|
||||
SEARCH_URL = "https://www.douban.com/j/search"
|
||||
SEARCH_JSON_URL = "https://www.douban.com/j/search"
|
||||
SEARCH_URL = "https://www.douban.com/search"
|
||||
|
||||
ID_PATTERN = re.compile(r"sid: (?P<id>\d+),")
|
||||
AUTHORS_PATTERN = re.compile(r"作者|译者")
|
||||
@@ -52,6 +53,7 @@ class Douban(Metadata):
|
||||
PUBLISHED_DATE_PATTERN = re.compile(r"出版年")
|
||||
SERIES_PATTERN = re.compile(r"丛书")
|
||||
IDENTIFIERS_PATTERN = re.compile(r"ISBN|统一书号")
|
||||
CRITERIA_PATTERN = re.compile("criteria = '(.+)'")
|
||||
|
||||
TITTLE_XPATH = "//span[@property='v:itemreviewed']"
|
||||
COVER_XPATH = "//a[@class='nbg']"
|
||||
@@ -63,56 +65,90 @@ class Douban(Metadata):
|
||||
session = requests.Session()
|
||||
session.headers = {
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.56',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.56',
|
||||
}
|
||||
|
||||
def search(
|
||||
self, query: str, generic_cover: str = "", locale: str = "en"
|
||||
) -> Optional[List[MetaRecord]]:
|
||||
def search(self,
|
||||
query: str,
|
||||
generic_cover: str = "",
|
||||
locale: str = "en") -> List[MetaRecord]:
|
||||
val = []
|
||||
if self.active:
|
||||
log.debug(f"starting search {query} on douban")
|
||||
log.debug(f"start searching {query} on douban")
|
||||
if title_tokens := list(
|
||||
self.get_title_tokens(query, strip_joiners=False)
|
||||
):
|
||||
self.get_title_tokens(query, strip_joiners=False)):
|
||||
query = "+".join(title_tokens)
|
||||
|
||||
try:
|
||||
r = self.session.get(
|
||||
self.SEARCH_URL, params={"cat": 1001, "q": query}
|
||||
)
|
||||
r.raise_for_status()
|
||||
book_id_list = self._get_book_id_list_from_html(query)
|
||||
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
return None
|
||||
|
||||
results = r.json()
|
||||
if results["total"] == 0:
|
||||
if not book_id_list:
|
||||
log.debug("No search results in Douban")
|
||||
return []
|
||||
|
||||
book_id_list = [
|
||||
self.ID_PATTERN.search(item).group("id")
|
||||
for item in results["items"][:10] if self.ID_PATTERN.search(item)
|
||||
]
|
||||
|
||||
with futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||
with futures.ThreadPoolExecutor(
|
||||
max_workers=5, thread_name_prefix='douban') as executor:
|
||||
|
||||
fut = [
|
||||
executor.submit(self._parse_single_book, book_id, generic_cover)
|
||||
for book_id in book_id_list
|
||||
executor.submit(self._parse_single_book, book_id,
|
||||
generic_cover) for book_id in book_id_list
|
||||
]
|
||||
|
||||
|
||||
val = [
|
||||
future.result()
|
||||
for future in futures.as_completed(fut) if future.result()
|
||||
future.result() for future in futures.as_completed(fut)
|
||||
if future.result()
|
||||
]
|
||||
|
||||
return val
|
||||
|
||||
def _parse_single_book(
|
||||
self, id: str, generic_cover: str = ""
|
||||
) -> Optional[MetaRecord]:
|
||||
def _get_book_id_list_from_html(self, query: str) -> List[str]:
|
||||
try:
|
||||
r = self.session.get(self.SEARCH_URL,
|
||||
params={
|
||||
"cat": 1001,
|
||||
"q": query
|
||||
})
|
||||
r.raise_for_status()
|
||||
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
return []
|
||||
|
||||
html = etree.HTML(r.content.decode("utf8"))
|
||||
result_list = html.xpath(self.COVER_XPATH)
|
||||
|
||||
return [
|
||||
self.ID_PATTERN.search(item.get("onclick")).group("id")
|
||||
for item in result_list[:10]
|
||||
if self.ID_PATTERN.search(item.get("onclick"))
|
||||
]
|
||||
|
||||
def _get_book_id_list_from_json(self, query: str) -> List[str]:
|
||||
try:
|
||||
r = self.session.get(self.SEARCH_JSON_URL,
|
||||
params={
|
||||
"cat": 1001,
|
||||
"q": query
|
||||
})
|
||||
r.raise_for_status()
|
||||
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
return []
|
||||
|
||||
results = r.json()
|
||||
if results["total"] == 0:
|
||||
return []
|
||||
|
||||
return [
|
||||
self.ID_PATTERN.search(item).group("id")
|
||||
for item in results["items"][:10] if self.ID_PATTERN.search(item)
|
||||
]
|
||||
|
||||
def _parse_single_book(self,
|
||||
id: str,
|
||||
generic_cover: str = "") -> Optional[MetaRecord]:
|
||||
url = f"https://book.douban.com/subject/{id}/"
|
||||
log.debug(f"start parsing {url}")
|
||||
|
||||
try:
|
||||
r = self.session.get(url)
|
||||
@@ -136,7 +172,8 @@ class Douban(Metadata):
|
||||
html = etree.HTML(r.content.decode("utf8"))
|
||||
|
||||
match.title = html.xpath(self.TITTLE_XPATH)[0].text
|
||||
match.cover = html.xpath(self.COVER_XPATH)[0].attrib["href"] or generic_cover
|
||||
match.cover = html.xpath(
|
||||
self.COVER_XPATH)[0].attrib["href"] or generic_cover
|
||||
try:
|
||||
rating_num = float(html.xpath(self.RATING_XPATH)[0].text.strip())
|
||||
except Exception:
|
||||
@@ -146,35 +183,39 @@ class Douban(Metadata):
|
||||
tag_elements = html.xpath(self.TAGS_XPATH)
|
||||
if len(tag_elements):
|
||||
match.tags = [tag_element.text for tag_element in tag_elements]
|
||||
else:
|
||||
match.tags = self._get_tags(html.text)
|
||||
|
||||
description_element = html.xpath(self.DESCRIPTION_XPATH)
|
||||
if len(description_element):
|
||||
match.description = html2text(etree.tostring(
|
||||
description_element[-1], encoding="utf8").decode("utf8"))
|
||||
match.description = html2text(
|
||||
etree.tostring(description_element[-1]).decode("utf8"))
|
||||
|
||||
info = html.xpath(self.INFO_XPATH)
|
||||
|
||||
for element in info:
|
||||
text = element.text
|
||||
if self.AUTHORS_PATTERN.search(text):
|
||||
next = element.getnext()
|
||||
while next is not None and next.tag != "br":
|
||||
match.authors.append(next.text)
|
||||
next = next.getnext()
|
||||
next_element = element.getnext()
|
||||
while next_element is not None and next_element.tag != "br":
|
||||
match.authors.append(next_element.text)
|
||||
next_element = next_element.getnext()
|
||||
elif self.PUBLISHER_PATTERN.search(text):
|
||||
match.publisher = element.tail.strip()
|
||||
if publisher := element.tail.strip():
|
||||
match.publisher = publisher
|
||||
else:
|
||||
match.publisher = element.getnext().text
|
||||
elif self.SUBTITLE_PATTERN.search(text):
|
||||
match.title = f'{match.title}:' + element.tail.strip()
|
||||
match.title = f'{match.title}:{element.tail.strip()}'
|
||||
elif self.PUBLISHED_DATE_PATTERN.search(text):
|
||||
match.publishedDate = self._clean_date(element.tail.strip())
|
||||
elif self.SUBTITLE_PATTERN.search(text):
|
||||
elif self.SERIES_PATTERN.search(text):
|
||||
match.series = element.getnext().text
|
||||
elif i_type := self.IDENTIFIERS_PATTERN.search(text):
|
||||
match.identifiers[i_type.group()] = element.tail.strip()
|
||||
|
||||
return match
|
||||
|
||||
|
||||
def _clean_date(self, date: str) -> str:
|
||||
"""
|
||||
Clean up the date string to be in the format YYYY-MM-DD
|
||||
@@ -194,13 +235,24 @@ class Douban(Metadata):
|
||||
if date[i].isdigit():
|
||||
digit.append(date[i])
|
||||
elif digit:
|
||||
ls.append("".join(digit) if len(digit)==2 else f"0{digit[0]}")
|
||||
ls.append("".join(digit) if len(digit) ==
|
||||
2 else f"0{digit[0]}")
|
||||
digit = []
|
||||
if digit:
|
||||
ls.append("".join(digit) if len(digit)==2 else f"0{digit[0]}")
|
||||
ls.append("".join(digit) if len(digit) ==
|
||||
2 else f"0{digit[0]}")
|
||||
|
||||
moon = ls[0]
|
||||
if len(ls)>1:
|
||||
day = ls[1]
|
||||
if len(ls) > 1:
|
||||
day = ls[1]
|
||||
|
||||
return f"{year}-{moon}-{day}"
|
||||
|
||||
def _get_tags(self, text: str) -> List[str]:
|
||||
tags = []
|
||||
if criteria := self.CRITERIA_PATTERN.search(text):
|
||||
tags.extend(
|
||||
item.replace('7:', '') for item in criteria.group().split('|')
|
||||
if item.startswith('7:'))
|
||||
|
||||
return tags
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# Google Books api document: https://developers.google.com/books/docs/v1/using
|
||||
from typing import Dict, List, Optional
|
||||
from urllib.parse import quote
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
|
||||
@@ -81,7 +82,11 @@ class Google(Metadata):
|
||||
match.description = result["volumeInfo"].get("description", "")
|
||||
match.languages = self._parse_languages(result=result, locale=locale)
|
||||
match.publisher = result["volumeInfo"].get("publisher", "")
|
||||
match.publishedDate = result["volumeInfo"].get("publishedDate", "")
|
||||
try:
|
||||
datetime.strptime(result["volumeInfo"].get("publishedDate", ""), "%Y-%m-%d")
|
||||
match.publishedDate = result["volumeInfo"].get("publishedDate", "")
|
||||
except ValueError:
|
||||
match.publishedDate = ""
|
||||
match.rating = result["volumeInfo"].get("averageRating", 0)
|
||||
match.series, match.series_index = "", 1
|
||||
match.tags = result["volumeInfo"].get("categories", [])
|
||||
@@ -103,6 +108,13 @@ class Google(Metadata):
|
||||
def _parse_cover(result: Dict, generic_cover: str) -> str:
|
||||
if result["volumeInfo"].get("imageLinks"):
|
||||
cover_url = result["volumeInfo"]["imageLinks"]["thumbnail"]
|
||||
|
||||
# strip curl in cover
|
||||
cover_url = cover_url.replace("&edge=curl", "")
|
||||
|
||||
# request 800x900 cover image (higher resolution)
|
||||
cover_url += "&fife=w800-h900"
|
||||
|
||||
return cover_url.replace("http://", "https://")
|
||||
return generic_cover
|
||||
|
||||
|
||||
@@ -49,10 +49,12 @@ class scholar(Metadata):
|
||||
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
|
||||
query = " ".join(tokens)
|
||||
try:
|
||||
scholarly.set_timeout(20)
|
||||
scholarly.set_retries(2)
|
||||
scholar_gen = itertools.islice(scholarly.search_pubs(query), 10)
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
return None
|
||||
return list()
|
||||
for result in scholar_gen:
|
||||
match = self._parse_search_result(
|
||||
result=result, generic_cover="", locale=locale
|
||||
|
||||
@@ -74,7 +74,7 @@ def register_user_with_oauth(user=None):
|
||||
if len(all_oauth.keys()) == 0:
|
||||
return
|
||||
if user is None:
|
||||
flash(_(u"Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success")
|
||||
flash(_("Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success")
|
||||
else:
|
||||
for oauth_key in all_oauth.keys():
|
||||
# Find this OAuth token in the database, or create it
|
||||
@@ -134,8 +134,8 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
|
||||
# already bind with user, just login
|
||||
if oauth_entry.user:
|
||||
login_user(oauth_entry.user)
|
||||
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.name)
|
||||
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.name),
|
||||
log.debug("You are now logged in as: '%s'", oauth_entry.user.name)
|
||||
flash(_("Success! You are now logged in as: %(nickname)s", nickname= oauth_entry.user.name),
|
||||
category="success")
|
||||
return redirect(url_for('web.index'))
|
||||
else:
|
||||
@@ -145,21 +145,21 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
|
||||
try:
|
||||
ub.session.add(oauth_entry)
|
||||
ub.session.commit()
|
||||
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
|
||||
flash(_("Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
|
||||
log.info("Link to {} Succeeded".format(provider_name))
|
||||
return redirect(url_for('web.profile'))
|
||||
except Exception as ex:
|
||||
log.error_or_exception(ex)
|
||||
ub.session.rollback()
|
||||
else:
|
||||
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
|
||||
flash(_("Login failed, No User Linked With OAuth Account"), category="error")
|
||||
log.info('Login failed, No User Linked With OAuth Account')
|
||||
return redirect(url_for('web.login'))
|
||||
# return redirect(url_for('web.login'))
|
||||
# if config.config_public_reg:
|
||||
# return redirect(url_for('web.register'))
|
||||
# else:
|
||||
# flash(_(u"Public registration is not enabled"), category="error")
|
||||
# flash(_("Public registration is not enabled"), category="error")
|
||||
# return redirect(url_for(redirect_url))
|
||||
except (NoResultFound, AttributeError):
|
||||
return redirect(url_for(redirect_url))
|
||||
@@ -194,15 +194,15 @@ def unlink_oauth(provider):
|
||||
ub.session.delete(oauth_entry)
|
||||
ub.session.commit()
|
||||
logout_oauth_user()
|
||||
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
|
||||
flash(_("Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
|
||||
log.info("Unlink to {} Succeeded".format(oauth_check[provider]))
|
||||
except Exception as ex:
|
||||
log.error_or_exception(ex)
|
||||
ub.session.rollback()
|
||||
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
|
||||
flash(_("Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
|
||||
except NoResultFound:
|
||||
log.warning("oauth %s for user %d not found", provider, current_user.id)
|
||||
flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error")
|
||||
flash(_("Not Linked to %(oauth)s", oauth=provider), category="error")
|
||||
return redirect(url_for('web.profile'))
|
||||
|
||||
def generate_oauth_blueprints():
|
||||
@@ -258,13 +258,13 @@ if ub.oauth_support:
|
||||
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
|
||||
def github_logged_in(blueprint, token):
|
||||
if not token:
|
||||
flash(_(u"Failed to log in with GitHub."), category="error")
|
||||
flash(_("Failed to log in with GitHub."), category="error")
|
||||
log.error("Failed to log in with GitHub")
|
||||
return False
|
||||
|
||||
resp = blueprint.session.get("/user")
|
||||
if not resp.ok:
|
||||
flash(_(u"Failed to fetch user info from GitHub."), category="error")
|
||||
flash(_("Failed to fetch user info from GitHub."), category="error")
|
||||
log.error("Failed to fetch user info from GitHub")
|
||||
return False
|
||||
|
||||
@@ -276,13 +276,13 @@ if ub.oauth_support:
|
||||
@oauth_authorized.connect_via(oauthblueprints[1]['blueprint'])
|
||||
def google_logged_in(blueprint, token):
|
||||
if not token:
|
||||
flash(_(u"Failed to log in with Google."), category="error")
|
||||
flash(_("Failed to log in with Google."), category="error")
|
||||
log.error("Failed to log in with Google")
|
||||
return False
|
||||
|
||||
resp = blueprint.session.get("/oauth2/v2/userinfo")
|
||||
if not resp.ok:
|
||||
flash(_(u"Failed to fetch user info from Google."), category="error")
|
||||
flash(_("Failed to fetch user info from Google."), category="error")
|
||||
log.error("Failed to fetch user info from Google")
|
||||
return False
|
||||
|
||||
@@ -295,8 +295,8 @@ if ub.oauth_support:
|
||||
@oauth_error.connect_via(oauthblueprints[0]['blueprint'])
|
||||
def github_error(blueprint, error, error_description=None, error_uri=None):
|
||||
msg = (
|
||||
u"OAuth error from {name}! "
|
||||
u"error={error} description={description} uri={uri}"
|
||||
"OAuth error from {name}! "
|
||||
"error={error} description={description} uri={uri}"
|
||||
).format(
|
||||
name=blueprint.name,
|
||||
error=error,
|
||||
@@ -308,8 +308,8 @@ if ub.oauth_support:
|
||||
@oauth_error.connect_via(oauthblueprints[1]['blueprint'])
|
||||
def google_error(blueprint, error, error_description=None, error_uri=None):
|
||||
msg = (
|
||||
u"OAuth error from {name}! "
|
||||
u"error={error} description={description} uri={uri}"
|
||||
"OAuth error from {name}! "
|
||||
"error={error} description={description} uri={uri}"
|
||||
).format(
|
||||
name=blueprint.name,
|
||||
error=error,
|
||||
@@ -329,10 +329,10 @@ def github_login():
|
||||
if account_info.ok:
|
||||
account_info_json = account_info.json()
|
||||
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
|
||||
flash(_(u"GitHub Oauth error, please retry later."), category="error")
|
||||
flash(_("GitHub Oauth error, please retry later."), category="error")
|
||||
log.error("GitHub Oauth error, please retry later")
|
||||
except (InvalidGrantError, TokenExpiredError) as e:
|
||||
flash(_(u"GitHub Oauth error: {}").format(e), category="error")
|
||||
flash(_("GitHub Oauth error: {}").format(e), category="error")
|
||||
log.error(e)
|
||||
return redirect(url_for('web.login'))
|
||||
|
||||
@@ -353,10 +353,10 @@ def google_login():
|
||||
if resp.ok:
|
||||
account_info_json = resp.json()
|
||||
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
|
||||
flash(_(u"Google Oauth error, please retry later."), category="error")
|
||||
flash(_("Google Oauth error, please retry later."), category="error")
|
||||
log.error("Google Oauth error, please retry later")
|
||||
except (InvalidGrantError, TokenExpiredError) as e:
|
||||
flash(_(u"Google Oauth error: {}").format(e), category="error")
|
||||
flash(_("Google Oauth error: {}").format(e), category="error")
|
||||
log.error(e)
|
||||
return redirect(url_for('web.login'))
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@ def feed_format(book_id):
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_languagesindex():
|
||||
off = request.args.get("offset") or 0
|
||||
if current_user.filter_language() == u"all":
|
||||
if current_user.filter_language() == "all":
|
||||
languages = calibre_db.speaking_language()
|
||||
else:
|
||||
languages = calibre_db.session.query(db.Languages).filter(
|
||||
|
||||
@@ -58,8 +58,8 @@ def remote_login():
|
||||
ub.session.add(auth_token)
|
||||
ub.session_commit()
|
||||
verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true)
|
||||
log.debug(u"Remot Login request with token: %s", auth_token.auth_token)
|
||||
return render_title_template('remote_login.html', title=_(u"Login"), token=auth_token.auth_token,
|
||||
log.debug("Remot Login request with token: %s", auth_token.auth_token)
|
||||
return render_title_template('remote_login.html', title=_("Login"), token=auth_token.auth_token,
|
||||
verify_url=verify_url, page="remotelogin")
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ def verify_token(token):
|
||||
|
||||
# Token not found
|
||||
if auth_token is None:
|
||||
flash(_(u"Token not found"), category="error")
|
||||
log.error(u"Remote Login token not found")
|
||||
flash(_("Token not found"), category="error")
|
||||
log.error("Remote Login token not found")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
# Token expired
|
||||
@@ -80,8 +80,8 @@ def verify_token(token):
|
||||
ub.session.delete(auth_token)
|
||||
ub.session_commit()
|
||||
|
||||
flash(_(u"Token has expired"), category="error")
|
||||
log.error(u"Remote Login token expired")
|
||||
flash(_("Token has expired"), category="error")
|
||||
log.error("Remote Login token expired")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
# Update token with user information
|
||||
@@ -89,8 +89,8 @@ def verify_token(token):
|
||||
auth_token.verified = True
|
||||
ub.session_commit()
|
||||
|
||||
flash(_(u"Success! Please return to your device"), category="success")
|
||||
log.debug(u"Remote Login token for userid %s verified", auth_token.user_id)
|
||||
flash(_("Success! Please return to your device"), category="success")
|
||||
log.debug("Remote Login token for userid %s verified", auth_token.user_id)
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ def token_verified():
|
||||
# Token not found
|
||||
if auth_token is None:
|
||||
data['status'] = 'error'
|
||||
data['message'] = _(u"Token not found")
|
||||
data['message'] = _("Token not found")
|
||||
|
||||
# Token expired
|
||||
elif datetime.now() > auth_token.expiration:
|
||||
@@ -113,7 +113,7 @@ def token_verified():
|
||||
ub.session_commit()
|
||||
|
||||
data['status'] = 'error'
|
||||
data['message'] = _(u"Token has expired")
|
||||
data['message'] = _("Token has expired")
|
||||
|
||||
elif not auth_token.verified:
|
||||
data['status'] = 'not_verified'
|
||||
@@ -126,8 +126,8 @@ def token_verified():
|
||||
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.name))
|
||||
|
||||
data['status'] = 'success'
|
||||
log.debug(u"Remote Login for userid %s succeded", user.id)
|
||||
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name), category="success")
|
||||
log.debug("Remote Login for userid %s succeeded", user.id)
|
||||
flash(_("Success! You are now logged in as: %(nickname)s", nickname=user.name), category="success")
|
||||
|
||||
response = make_response(json.dumps(data, ensure_ascii=False))
|
||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
|
||||
@@ -59,7 +59,7 @@ def get_sidebar_config(kwargs=None):
|
||||
"show_text": _('Show Top Rated Books'), "config_show": True})
|
||||
sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read",
|
||||
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous),
|
||||
"page": "read", "show_text": _('Show read and unread'), "config_show": content})
|
||||
"page": "read", "show_text": _('Show Read and Unread'), "config_show": content})
|
||||
sidebar.append(
|
||||
{"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread",
|
||||
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread",
|
||||
@@ -69,31 +69,31 @@ def get_sidebar_config(kwargs=None):
|
||||
"show_text": _('Show Random Books'), "config_show": True})
|
||||
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
|
||||
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
|
||||
"show_text": _('Show category selection'), "config_show": True})
|
||||
"show_text": _('Show Category Section'), "config_show": True})
|
||||
sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie",
|
||||
"visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series",
|
||||
"show_text": _('Show series selection'), "config_show": True})
|
||||
"show_text": _('Show Series Section'), "config_show": True})
|
||||
sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author",
|
||||
"visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author",
|
||||
"show_text": _('Show author selection'), "config_show": True})
|
||||
"show_text": _('Show Author Section'), "config_show": True})
|
||||
sidebar.append(
|
||||
{"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher",
|
||||
"visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher",
|
||||
"show_text": _('Show publisher selection'), "config_show":True})
|
||||
"show_text": _('Show Publisher Section'), "config_show":True})
|
||||
sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang",
|
||||
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'),
|
||||
"page": "language",
|
||||
"show_text": _('Show language selection'), "config_show": True})
|
||||
"show_text": _('Show Language Section'), "config_show": True})
|
||||
sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate",
|
||||
"visibility": constants.SIDEBAR_RATING, 'public': True,
|
||||
"page": "rating", "show_text": _('Show ratings selection'), "config_show": True})
|
||||
"page": "rating", "show_text": _('Show Ratings Section'), "config_show": True})
|
||||
sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format",
|
||||
"visibility": constants.SIDEBAR_FORMAT, 'public': True,
|
||||
"page": "format", "show_text": _('Show file formats selection'), "config_show": True})
|
||||
"page": "format", "show_text": _('Show File Formats Section'), "config_show": True})
|
||||
sidebar.append(
|
||||
{"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
|
||||
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived",
|
||||
"show_text": _('Show archived books'), "config_show": content})
|
||||
"show_text": _('Show Archived Books'), "config_show": content})
|
||||
if not simple:
|
||||
sidebar.append(
|
||||
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
import datetime
|
||||
|
||||
from . import config, constants
|
||||
from .services.background_scheduler import BackgroundScheduler, use_APScheduler
|
||||
from .services.background_scheduler import BackgroundScheduler, CronTrigger, use_APScheduler
|
||||
from .tasks.database import TaskReconnectDatabase
|
||||
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails, TaskClearCoverThumbnailCache
|
||||
from .services.worker import WorkerThread
|
||||
|
||||
from .tasks.metadata_backup import TaskBackupMetadata
|
||||
|
||||
def get_scheduled_tasks(reconnect=True):
|
||||
tasks = list()
|
||||
@@ -32,6 +32,10 @@ def get_scheduled_tasks(reconnect=True):
|
||||
if reconnect:
|
||||
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
|
||||
|
||||
# ToDo make configurable. Generate metadata.opf file for each changed book
|
||||
if False:
|
||||
tasks.append([lambda: TaskBackupMetadata("en"), 'backup metadata', False])
|
||||
|
||||
# Generate all missing book cover thumbnails
|
||||
if config.schedule_generate_book_covers:
|
||||
tasks.append([lambda: TaskClearCoverThumbnailCache(0), 'delete superfluous book covers', True])
|
||||
@@ -62,10 +66,10 @@ def register_scheduled_tasks(reconnect=True):
|
||||
duration = config.schedule_duration
|
||||
|
||||
# Register scheduled tasks
|
||||
scheduler.schedule_tasks(tasks=get_scheduled_tasks(reconnect), trigger='cron', hour=start)
|
||||
scheduler.schedule_tasks(tasks=get_scheduled_tasks(reconnect), trigger=CronTrigger(hour=start))
|
||||
end_time = calclulate_end_time(start, duration)
|
||||
scheduler.schedule(func=end_scheduled_tasks, trigger='cron', name="end scheduled task", hour=end_time.hour,
|
||||
minute=end_time.minute)
|
||||
scheduler.schedule(func=end_scheduled_tasks, trigger=CronTrigger(hour=end_time.hour, minute=end_time.minute),
|
||||
name="end scheduled task")
|
||||
|
||||
# Kick-off tasks, if they should currently be running
|
||||
if should_task_be_running(start, duration):
|
||||
@@ -91,6 +95,7 @@ def should_task_be_running(start, duration):
|
||||
end_time = start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60)
|
||||
return start_time < now < end_time
|
||||
|
||||
|
||||
def calclulate_end_time(start, duration):
|
||||
start_time = datetime.datetime.now().replace(hour=start, minute=0)
|
||||
return start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60)
|
||||
|
||||
@@ -45,7 +45,7 @@ def simple_search():
|
||||
return render_title_template('search.html',
|
||||
searchterm="",
|
||||
result_count=0,
|
||||
title=_(u"Search"),
|
||||
title=_("Search"),
|
||||
page="search")
|
||||
|
||||
|
||||
@@ -134,9 +134,10 @@ def adv_search_read_status(read_status):
|
||||
db_filter = coalesce(db.cc_classes[config.config_read_column].value, False) != True
|
||||
except (KeyError, AttributeError, IndexError):
|
||||
log.error("Custom Column No.{} does not exist in calibre database".format(config.config_read_column))
|
||||
flash(_("Custom Column No.{} does not exist in calibre database".format(config.config_read_column)),
|
||||
category="error")
|
||||
return false()
|
||||
flash(_("Custom Column No.%(column)d does not exist in calibre database",
|
||||
column=config.config_read_column),
|
||||
category="error")
|
||||
return true()
|
||||
return db_filter
|
||||
|
||||
|
||||
@@ -184,18 +185,18 @@ def extend_search_term(searchterm,
|
||||
searchterm.extend((author_name.replace('|', ','), book_title, publisher))
|
||||
if pub_start:
|
||||
try:
|
||||
searchterm.extend([_(u"Published after ") +
|
||||
searchterm.extend([_("Published after ") +
|
||||
format_date(datetime.strptime(pub_start, "%Y-%m-%d"),
|
||||
format='medium')])
|
||||
except ValueError:
|
||||
pub_start = u""
|
||||
pub_start = ""
|
||||
if pub_end:
|
||||
try:
|
||||
searchterm.extend([_(u"Published before ") +
|
||||
searchterm.extend([_("Published before ") +
|
||||
format_date(datetime.strptime(pub_end, "%Y-%m-%d"),
|
||||
format='medium')])
|
||||
except ValueError:
|
||||
pub_end = u""
|
||||
pub_end = ""
|
||||
elements = {'tag': db.Tags, 'serie':db.Series, 'shelf':ub.Shelf}
|
||||
for key, db_element in elements.items():
|
||||
tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['include_' + key])).all()
|
||||
@@ -213,11 +214,11 @@ def extend_search_term(searchterm,
|
||||
language_names = calibre_db.speaking_language(language_names)
|
||||
searchterm.extend(language.name for language in language_names)
|
||||
if rating_high:
|
||||
searchterm.extend([_(u"Rating <= %(rating)s", rating=rating_high)])
|
||||
searchterm.extend([_("Rating <= %(rating)s", rating=rating_high)])
|
||||
if rating_low:
|
||||
searchterm.extend([_(u"Rating >= %(rating)s", rating=rating_low)])
|
||||
searchterm.extend([_("Rating >= %(rating)s", rating=rating_low)])
|
||||
if read_status:
|
||||
searchterm.extend([_(u"Read Status = %(status)s", status=read_status)])
|
||||
searchterm.extend([_("Read Status = %(status)s", status=read_status)])
|
||||
searchterm.extend(ext for ext in tags['include_extension'])
|
||||
searchterm.extend(ext for ext in tags['exclude_extension'])
|
||||
# handle custom columns
|
||||
@@ -266,19 +267,19 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
||||
column_start = term.get('custom_column_' + str(c.id) + '_start')
|
||||
column_end = term.get('custom_column_' + str(c.id) + '_end')
|
||||
if column_start:
|
||||
search_term.extend([u"{} >= {}".format(c.name,
|
||||
search_term.extend(["{} >= {}".format(c.name,
|
||||
format_date(datetime.strptime(column_start, "%Y-%m-%d").date(),
|
||||
format='medium')
|
||||
)])
|
||||
cc_present = True
|
||||
if column_end:
|
||||
search_term.extend([u"{} <= {}".format(c.name,
|
||||
search_term.extend(["{} <= {}".format(c.name,
|
||||
format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
|
||||
format='medium')
|
||||
)])
|
||||
cc_present = True
|
||||
elif term.get('custom_column_' + str(c.id)):
|
||||
search_term.extend([(u"{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
|
||||
search_term.extend([("{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
|
||||
cc_present = True
|
||||
|
||||
if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \
|
||||
@@ -338,7 +339,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
||||
pagination=pagination,
|
||||
entries=entries,
|
||||
result_count=result_count,
|
||||
title=_(u"Advanced Search"), page="advsearch",
|
||||
title=_("Advanced Search"), page="advsearch",
|
||||
order=order[1])
|
||||
|
||||
|
||||
@@ -365,12 +366,12 @@ def render_prepare_search_form(cc):
|
||||
.filter(calibre_db.common_filters()) \
|
||||
.group_by(db.Data.format)\
|
||||
.order_by(db.Data.format).all()
|
||||
if current_user.filter_language() == u"all":
|
||||
if current_user.filter_language() == "all":
|
||||
languages = calibre_db.speaking_language()
|
||||
else:
|
||||
languages = None
|
||||
return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions,
|
||||
series=series,shelves=shelves, title=_(u"Advanced Search"), cc=cc, page="advsearch")
|
||||
series=series,shelves=shelves, title=_("Advanced Search"), cc=cc, page="advsearch")
|
||||
|
||||
|
||||
def render_search_results(term, offset=None, order=None, limit=None):
|
||||
@@ -388,7 +389,7 @@ def render_search_results(term, offset=None, order=None, limit=None):
|
||||
adv_searchterm=term,
|
||||
entries=entries,
|
||||
result_count=result_count,
|
||||
title=_(u"Search"),
|
||||
title=_("Search"),
|
||||
page="search",
|
||||
order=order[1])
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ from .worker import WorkerThread
|
||||
|
||||
try:
|
||||
from apscheduler.schedulers.background import BackgroundScheduler as BScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from apscheduler.triggers.date import DateTrigger
|
||||
use_APScheduler = True
|
||||
except (ImportError, RuntimeError) as e:
|
||||
use_APScheduler = False
|
||||
@@ -47,31 +49,31 @@ class BackgroundScheduler:
|
||||
|
||||
return cls._instance
|
||||
|
||||
def schedule(self, func, trigger, name=None, **trigger_args):
|
||||
def schedule(self, func, trigger, name=None):
|
||||
if use_APScheduler:
|
||||
return self.scheduler.add_job(func=func, trigger=trigger, name=name, **trigger_args)
|
||||
return self.scheduler.add_job(func=func, trigger=trigger, name=name)
|
||||
|
||||
# Expects a lambda expression for the task
|
||||
def schedule_task(self, task, user=None, name=None, hidden=False, trigger='cron', **trigger_args):
|
||||
def schedule_task(self, task, user=None, name=None, hidden=False, trigger=None):
|
||||
if use_APScheduler:
|
||||
def scheduled_task():
|
||||
worker_task = task()
|
||||
worker_task.scheduled = True
|
||||
WorkerThread.add(user, worker_task, hidden=hidden)
|
||||
return self.schedule(func=scheduled_task, trigger=trigger, name=name, **trigger_args)
|
||||
return self.schedule(func=scheduled_task, trigger=trigger, name=name)
|
||||
|
||||
# Expects a list of lambda expressions for the tasks
|
||||
def schedule_tasks(self, tasks, user=None, trigger='cron', **trigger_args):
|
||||
def schedule_tasks(self, tasks, user=None, trigger=None):
|
||||
if use_APScheduler:
|
||||
for task in tasks:
|
||||
self.schedule_task(task[0], user=user, trigger=trigger, name=task[1], hidden=task[2], **trigger_args)
|
||||
self.schedule_task(task[0], user=user, trigger=trigger, name=task[1], hidden=task[2])
|
||||
|
||||
# Expects a lambda expression for the task
|
||||
def schedule_task_immediately(self, task, user=None, name=None, hidden=False):
|
||||
if use_APScheduler:
|
||||
def immediate_task():
|
||||
WorkerThread.add(user, task(), hidden)
|
||||
return self.schedule(func=immediate_task, trigger='date', name=name)
|
||||
return self.schedule(func=immediate_task, trigger=DateTrigger(), name=name)
|
||||
|
||||
# Expects a list of lambda expressions for the tasks
|
||||
def schedule_tasks_immediately(self, tasks, user=None):
|
||||
|
||||
@@ -46,13 +46,13 @@ def add_to_shelf(shelf_id, book_id):
|
||||
if shelf is None:
|
||||
log.error("Invalid shelf specified: %s", shelf_id)
|
||||
if not xhr:
|
||||
flash(_(u"Invalid shelf specified"), category="error")
|
||||
flash(_("Invalid shelf specified"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
return "Invalid shelf specified", 400
|
||||
|
||||
if not check_shelf_edit_permissions(shelf):
|
||||
if not xhr:
|
||||
flash(_(u"Sorry you are not allowed to add a book to that shelf"), category="error")
|
||||
flash(_("Sorry you are not allowed to add a book to that shelf"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
return "Sorry you are not allowed to add a book to the that shelf", 403
|
||||
|
||||
@@ -61,7 +61,7 @@ def add_to_shelf(shelf_id, book_id):
|
||||
if book_in_shelf:
|
||||
log.error("Book %s is already part of %s", book_id, shelf)
|
||||
if not xhr:
|
||||
flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error")
|
||||
flash(_("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
|
||||
|
||||
@@ -79,14 +79,14 @@ def add_to_shelf(shelf_id, book_id):
|
||||
except (OperationalError, InvalidRequestError) as e:
|
||||
ub.session.rollback()
|
||||
log.error_or_exception("Settings Database error: {}".format(e))
|
||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||
if "HTTP_REFERER" in request.environ:
|
||||
return redirect(request.environ["HTTP_REFERER"])
|
||||
else:
|
||||
return redirect(url_for('web.index'))
|
||||
if not xhr:
|
||||
log.debug("Book has been added to shelf: {}".format(shelf.name))
|
||||
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
||||
flash(_("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"])
|
||||
else:
|
||||
@@ -100,12 +100,12 @@ def search_to_shelf(shelf_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
if shelf is None:
|
||||
log.error("Invalid shelf specified: {}".format(shelf_id))
|
||||
flash(_(u"Invalid shelf specified"), category="error")
|
||||
flash(_("Invalid shelf specified"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
if not check_shelf_edit_permissions(shelf):
|
||||
log.warning("You are not allowed to add a book to the shelf".format(shelf.name))
|
||||
flash(_(u"You are not allowed to add a book to the shelf"), category="error")
|
||||
flash(_("You are not allowed to add a book to the shelf"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]:
|
||||
@@ -123,7 +123,7 @@ def search_to_shelf(shelf_id):
|
||||
|
||||
if not books_for_shelf:
|
||||
log.error("Books are already part of {}".format(shelf.name))
|
||||
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
|
||||
flash(_("Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0
|
||||
@@ -135,14 +135,14 @@ def search_to_shelf(shelf_id):
|
||||
try:
|
||||
ub.session.merge(shelf)
|
||||
ub.session.commit()
|
||||
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
||||
flash(_("Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
||||
except (OperationalError, InvalidRequestError) as e:
|
||||
ub.session.rollback()
|
||||
log.error_or_exception("Settings Database error: {}".format(e))
|
||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||
else:
|
||||
log.error("Could not add books to shelf: {}".format(shelf.name))
|
||||
flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
|
||||
flash(_("Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
|
||||
@@ -182,13 +182,13 @@ def remove_from_shelf(shelf_id, book_id):
|
||||
except (OperationalError, InvalidRequestError) as e:
|
||||
ub.session.rollback()
|
||||
log.error_or_exception("Settings Database error: {}".format(e))
|
||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||
if "HTTP_REFERER" in request.environ:
|
||||
return redirect(request.environ["HTTP_REFERER"])
|
||||
else:
|
||||
return redirect(url_for('web.index'))
|
||||
if not xhr:
|
||||
flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success")
|
||||
flash(_("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:
|
||||
@@ -197,7 +197,7 @@ def remove_from_shelf(shelf_id, book_id):
|
||||
else:
|
||||
if not xhr:
|
||||
log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name))
|
||||
flash(_(u"Sorry you are not allowed to remove a book from this shelf"),
|
||||
flash(_("Sorry you are not allowed to remove a book from this shelf"),
|
||||
category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
return "Sorry you are not allowed to remove a book from this shelf", 403
|
||||
@@ -207,7 +207,7 @@ def remove_from_shelf(shelf_id, book_id):
|
||||
@login_required
|
||||
def create_shelf():
|
||||
shelf = ub.Shelf()
|
||||
return create_edit_shelf(shelf, page_title=_(u"Create a Shelf"), page="shelfcreate")
|
||||
return create_edit_shelf(shelf, page_title=_("Create a Shelf"), page="shelfcreate")
|
||||
|
||||
|
||||
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
|
||||
@@ -215,9 +215,9 @@ def create_shelf():
|
||||
def edit_shelf(shelf_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
if not check_shelf_edit_permissions(shelf):
|
||||
flash(_(u"Sorry you are not allowed to edit this shelf"), category="error")
|
||||
flash(_("Sorry you are not allowed to edit this shelf"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
return create_edit_shelf(shelf, page_title=_(u"Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
|
||||
return create_edit_shelf(shelf, page_title=_("Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
|
||||
|
||||
|
||||
@shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
|
||||
@@ -232,7 +232,7 @@ def delete_shelf(shelf_id):
|
||||
except InvalidRequestError as e:
|
||||
ub.session.rollback()
|
||||
log.error_or_exception("Settings Database error: {}".format(e))
|
||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
|
||||
@@ -263,13 +263,13 @@ def order_shelf(shelf_id):
|
||||
for book in books_in_shelf:
|
||||
setattr(book, 'order', to_save[str(book.book_id)])
|
||||
counter += 1
|
||||
# if order diffrent from before -> shelf.last_modified = datetime.utcnow()
|
||||
# if order different from before -> shelf.last_modified = datetime.utcnow()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except (OperationalError, InvalidRequestError) as e:
|
||||
ub.session.rollback()
|
||||
log.error_or_exception("Settings Database error: {}".format(e))
|
||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||
|
||||
result = list()
|
||||
if shelf:
|
||||
@@ -278,7 +278,7 @@ def order_shelf(shelf_id):
|
||||
.add_columns(calibre_db.common_filters().label("visible")) \
|
||||
.filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all()
|
||||
return render_title_template('shelf_order.html', entries=result,
|
||||
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
|
||||
title=_("Change order of Shelf: '%(name)s'", name=shelf.name),
|
||||
shelf=shelf, page="shelforder")
|
||||
else:
|
||||
abort(404)
|
||||
@@ -310,7 +310,7 @@ def create_edit_shelf(shelf, page_title, page, shelf_id=False):
|
||||
if request.method == "POST":
|
||||
to_save = request.form.to_dict()
|
||||
if not current_user.role_edit_shelfs() and to_save.get("is_public") == "on":
|
||||
flash(_(u"Sorry you are not allowed to create a public shelf"), category="error")
|
||||
flash(_("Sorry you are not allowed to create a public shelf"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
is_public = 1 if to_save.get("is_public") == "on" else 0
|
||||
if config.config_kobo_sync:
|
||||
@@ -327,24 +327,24 @@ def create_edit_shelf(shelf, page_title, page, shelf_id=False):
|
||||
shelf.user_id = int(current_user.id)
|
||||
ub.session.add(shelf)
|
||||
shelf_action = "created"
|
||||
flash_text = _(u"Shelf %(title)s created", title=shelf_title)
|
||||
flash_text = _("Shelf %(title)s created", title=shelf_title)
|
||||
else:
|
||||
shelf_action = "changed"
|
||||
flash_text = _(u"Shelf %(title)s changed", title=shelf_title)
|
||||
flash_text = _("Shelf %(title)s changed", title=shelf_title)
|
||||
try:
|
||||
ub.session.commit()
|
||||
log.info(u"Shelf {} {}".format(shelf_title, shelf_action))
|
||||
log.info("Shelf {} {}".format(shelf_title, shelf_action))
|
||||
flash(flash_text, category="success")
|
||||
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
|
||||
except (OperationalError, InvalidRequestError) as ex:
|
||||
ub.session.rollback()
|
||||
log.error_or_exception(ex)
|
||||
log.error_or_exception("Settings Database error: {}".format(ex))
|
||||
flash(_(u"Database error: %(error)s.", error=ex.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=ex.orig), category="error")
|
||||
except Exception as ex:
|
||||
ub.session.rollback()
|
||||
log.error_or_exception(ex)
|
||||
flash(_(u"There was an error"), category="error")
|
||||
flash(_("There was an error"), category="error")
|
||||
return render_title_template('shelf_edit.html',
|
||||
shelf=shelf,
|
||||
title=page_title,
|
||||
@@ -366,7 +366,7 @@ def check_shelf_is_unique(title, is_public, shelf_id=False):
|
||||
|
||||
if not is_shelf_name_unique:
|
||||
log.error("A public shelf with the name '{}' already exists.".format(title))
|
||||
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=title),
|
||||
flash(_("A public shelf with the name '%(title)s' already exists.", title=title),
|
||||
category="error")
|
||||
else:
|
||||
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
||||
@@ -377,7 +377,7 @@ def check_shelf_is_unique(title, is_public, shelf_id=False):
|
||||
|
||||
if not is_shelf_name_unique:
|
||||
log.error("A private shelf with the name '{}' already exists.".format(title))
|
||||
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=title),
|
||||
flash(_("A private shelf with the name '%(title)s' already exists.", title=title),
|
||||
category="error")
|
||||
return is_shelf_name_unique
|
||||
|
||||
@@ -454,14 +454,14 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
|
||||
except (OperationalError, InvalidRequestError) as e:
|
||||
ub.session.rollback()
|
||||
log.error_or_exception("Settings Database error: {}".format(e))
|
||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||
|
||||
return render_title_template(page,
|
||||
entries=result,
|
||||
pagination=pagination,
|
||||
title=_(u"Shelf: '%(name)s'", name=shelf.name),
|
||||
title=_("Shelf: '%(name)s'", name=shelf.name),
|
||||
shelf=shelf,
|
||||
page="shelf")
|
||||
else:
|
||||
flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error")
|
||||
flash(_("Error opening shelf. Shelf does not exist or is not accessible"), category="error")
|
||||
return redirect(url_for("web.index"))
|
||||
|
||||
@@ -3290,9 +3290,11 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd
|
||||
-ms-transform-origin: center top;
|
||||
transform-origin: center top;
|
||||
border: 0;
|
||||
left: 0 !important;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.dropdown-menu:not(.datepicker-dropdown):not(.profileDropli) {
|
||||
left: 0 !important;
|
||||
}
|
||||
#add-to-shelves {
|
||||
max-height: calc(100% - 120px);
|
||||
overflow-y: auto;
|
||||
@@ -4423,38 +4425,6 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div
|
||||
left: 49px;
|
||||
margin-top: 5px
|
||||
}
|
||||
|
||||
body:not(.blur) > .navbar > .container-fluid > .navbar-header:after, body:not(.blur) > .navbar > .container-fluid > .navbar-header:before {
|
||||
color: hsla(0, 0%, 100%, .7);
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-size: 20px;
|
||||
font-stretch: 100%;
|
||||
font-style: normal;
|
||||
font-variant-caps: normal;
|
||||
font-variant-east-asian: normal;
|
||||
font-variant-numeric: normal;
|
||||
font-weight: 400;
|
||||
height: 60px;
|
||||
letter-spacing: normal;
|
||||
line-height: 60px;
|
||||
position: absolute
|
||||
}
|
||||
|
||||
body:not(.blur) > .navbar > .container-fluid > .navbar-header:before {
|
||||
content: "\EA30";
|
||||
-webkit-font-variant-ligatures: normal;
|
||||
font-variant-ligatures: normal;
|
||||
left: 20px
|
||||
}
|
||||
|
||||
body:not(.blur) > .navbar > .container-fluid > .navbar-header:after {
|
||||
content: "\EA2F";
|
||||
-webkit-font-variant-ligatures: normal;
|
||||
font-variant-ligatures: normal;
|
||||
left: 60px
|
||||
}
|
||||
}
|
||||
|
||||
body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row:first-of-type > div.col > h2:before, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > h2:first-of-type:before, body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > h1:before, body.newuser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > h1:before {
|
||||
|
||||
@@ -22,3 +22,7 @@ body.serieslist.grid-view div.container-fluid > div > div.col-sm-10::before {
|
||||
padding: 0 0;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
input.datepicker {color: transparent}
|
||||
input.datepicker:focus {color: transparent}
|
||||
input.datepicker:focus + input {color: #555}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M8 12a1 1 0 0 1-.707-.293l-5-5a1 1 0 0 1 1.414-1.414L8
|
||||
9.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-5 5A1 1 0 0 1 8 12z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 461 B |
@@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M13 11a1 1 0 0 1-.707-.293L8 6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414l5-5a1 1 0 0 1 1.414 0l5 5A1 1 0 0 1 13 11z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 458 B |
|
Before Width: | Height: | Size: 326 B |
|
Before Width: | Height: | Size: 326 B |
@@ -1,16 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16
|
||||
16"
|
||||
fill="rgba(255,255,255,1)">
|
||||
<path
|
||||
d="M8 16a8 8 0 1 1 8-8 8.009 8.009 0 0 1-8 8zM8 2a6 6 0 1 0 6 6 6.006 6.006 0 0 0-6-6z">
|
||||
</path>
|
||||
<path
|
||||
d="M8 7a1 1 0 0 0-1 1v3a1 1 0 0 0 2 0V8a1 1 0 0 0-1-1z">
|
||||
</path>
|
||||
<circle
|
||||
cx="8" cy="5" r="1.188">
|
||||
</circle>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 557 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M13 13c-.3 0-.5-.1-.7-.3L8 8.4l-4.3 4.3c-.9.9-2.3-.5-1.4-1.4l5-5c.4-.4 1-.4 1.4 0l5 5c.6.6.2 1.7-.7 1.7zm0-11H3C1.7 2 1.7 4 3 4h10c1.3 0 1.3-2 0-2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 255 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M15 3.7V13c0 1.5-1.53 3-3 3H7.13c-.72 0-1.63-.5-2.13-1l-5-5s.84-1 .87-1c.13-.1.33-.2.53-.2.1 0 .3.1.4.2L4 10.6V2.7c0-.6.4-1 1-1s1 .4 1 1v4.6h1V1c0-.6.4-1 1-1s1 .4 1 1v6.3h1V1.7c0-.6.4-1 1-1s1 .4 1 1v5.7h1V3.7c0-.6.4-1 1-1s1 .4 1 1z"/></svg>
|
||||
|
Before Width: | Height: | Size: 339 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M8 10c-.3 0-.5-.1-.7-.3l-5-5c-.9-.9.5-2.3 1.4-1.4L8 7.6l4.3-4.3c.9-.9 2.3.5 1.4 1.4l-5 5c-.2.2-.4.3-.7.3zm5 2H3c-1.3 0-1.3 2 0 2h10c1.3 0 1.3-2 0-2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 256 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M1 1a1 1 0 011 1v2.4A7 7 0 118 15a7 7 0 01-4.9-2 1 1 0 011.4-1.5 5 5 0 10-1-5.5H6a1 1 0 010 2H1a1 1 0 01-1-1V2a1 1 0 011-1z"/></svg>
|
||||
|
Before Width: | Height: | Size: 231 B |
@@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M15 1a1 1 0 0 0-1 1v2.418A6.995 6.995 0 1 0 8 15a6.954 6.954 0 0 0 4.95-2.05 1 1 0 0 0-1.414-1.414A5.019 5.019 0 1 1 12.549 6H10a1 1 0 0 0 0 2h5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 521 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M0 4h1.5c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5H0zM9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM16 4h-1.5c-1 0-1.5.5-1.5 1.5v5c0 1 .5 1.5 1.5 1.5H16z"/></svg>
|
||||
|
Before Width: | Height: | Size: 302 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4z"/></svg>
|
||||
|
After Width: | Height: | Size: 171 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM11 0v.5c0 1-.5 1.5-1.5 1.5h-3C5.5 2 5 1.5 5 .5V0h6zM11 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 307 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M5.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C1 4.5 1.5 4 2.5 4zM7 0v.5C7 1.5 6.5 2 5.5 2h-3C1.5 2 1 1.5 1 .5V0h6zM7 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6zM13.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5c0-1 .5-1.5 1.5-1.5zM15 0v.5c0 1-.5 1.5-1.5 1.5h-3C9.5 2 9 1.5 9 .5V0h6zM15 16v-.507c0-1-.5-1.5-1.5-1.5h-3C9.5 14 9 14.5 9 15.5v.5h6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 509 B |
@@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M12.408 8.217l-8.083-6.7A.2.2 0 0 0 4 1.672V12.3a.2.2 0 0 0 .333.146l2.56-2.372 1.857 3.9A1.125 1.125 0 1 0 10.782 13L8.913 9.075l3.4-.51a.2.2 0 0 0 .095-.348z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 505 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M1.5 3.5C.5 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm2 1.2c.8 0 1.4.2 1.8.6.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.4-.2.3-.5.7-1 1l-.6.4c-.4.3-.6.4-.75.56-.15.14-.25.24-.35.44H6v1.3H1c0-.6.1-1.1.3-1.5.3-.6.7-1 1.5-1.6.7-.4 1.1-.8 1.28-1 .32-.3.42-.6.42-1 0-.3-.1-.6-.23-.8-.17-.2-.37-.3-.77-.3s-.7.1-.9.5c-.04.2-.1.5-.1.9H1.1c0-.6.1-1.1.3-1.5.4-.7 1.1-1.1 2.1-1.1zM10.54 3.54C9.5 3.54 9 4 9 5v6.5c0 1 .5 1.5 1.54 1.5h4c.96 0 1.46-.5 1.46-1.5V5c0-1-.5-1.46-1.5-1.46zm1.9.95c.7 0 1.3.2 1.7.5.4.4.6.8.6 1.4 0 .4-.1.8-.4 1.1-.2.2-.3.3-.5.4.1 0 .3.1.6.3.4.3.5.8.5 1.4 0 .6-.2 1.2-.6 1.6-.4.5-1.1.7-1.9.7-1 0-1.8-.3-2.2-1-.14-.29-.24-.69-.24-1.29h1.4c0 .3 0 .5.1.7.2.4.5.5 1 .5.3 0 .5-.1.7-.3.2-.2.3-.5.3-.8 0-.5-.2-.8-.6-.95-.2-.05-.5-.15-1-.15v-1c.5 0 .8-.1 1-.14.3-.1.5-.4.5-.9 0-.3-.1-.5-.2-.7-.2-.2-.4-.3-.7-.3-.3 0-.6.1-.75.3-.2.2-.2.5-.2.86h-1.34c0-.4.1-.7.19-1.1 0-.12.2-.32.4-.62.2-.2.4-.3.7-.4.3-.1.6-.1 1-.1z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M6 3c-1 0-1.5.5-1.5 1.5v7c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5v-7c0-1-.5-1.5-1.5-1.5z"/></svg>
|
||||
|
Before Width: | Height: | Size: 196 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M10.56 3.5C9.56 3.5 9 4 9 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.93 1.2c.8 0 1.4.2 1.8.64.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.44-.2.3-.6.6-1 .93l-.6.4c-.4.3-.6.4-.7.55-.1.1-.2.2-.3.4h3.2v1.27h-5c0-.5.1-1 .3-1.43.2-.49.7-1 1.5-1.54.7-.5 1.1-.8 1.3-1.02.3-.3.4-.7.4-1.05 0-.3-.1-.6-.3-.77-.2-.2-.4-.3-.7-.3-.4 0-.7.2-.9.5-.1.2-.1.5-.2.9h-1.4c0-.6.2-1.1.3-1.5.4-.7 1.1-1.1 2-1.1zM1.54 3.5C.54 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.54 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.8 1.125H4.5V12H3V6.9H1.3v-1c.5 0 .8 0 .97-.03.33-.07.53-.17.73-.37.1-.2.2-.3.25-.5.05-.2.05-.3.05-.3z"/></svg>
|
||||
|
Before Width: | Height: | Size: 705 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M4 16V2s0-1 1-1h6s1 0 1 1v14l-4-5z"/></svg>
|
||||
|
Before Width: | Height: | Size: 142 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="m14 9h-6c-1.3 0-1.3 2 0 2h6c1.3 0 1.3-2 0-2zm-5.2-8h-3.8c-1.3 0-1.3 2 0 2h1.7zm-6.8 0c-1 0-1.3 1-0.7 1.7 0.7 0.6 1.7 0.3 1.7-0.7 0-0.5-0.4-1-1-1zm3 8c-1 0-1.3 1-0.7 1.7 0.6 0.6 1.7 0.2 1.7-0.7 0-0.5-0.4-1-1-1zm0.3-4h-0.3c-1.4 0-1.4 2 0 2h2.3zm-3.3 0c-0.9 0-1.4 1-0.7 1.7 0.7 0.6 1.7 0.2 1.7-0.7 0-0.6-0.5-1-1-1zm12 8h-9c-1.3 0-1.3 2 0 2h9c1.3 0 1.3-2 0-2zm-12 0c-1 0-1.3 1-0.7 1.7 0.7 0.6 1.7 0.2 1.7-0.712 0-0.5-0.4-1-1-1z"/><path d="m7.37 4.838 3.93-3.911v2.138h3.629v3.546h-3.629v2.138l-3.93-3.911"/></svg>
|
||||
|
After Width: | Height: | Size: 581 B |
@@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M14 3h-2v2h2v8H2V5h7V3h-.849L6.584 1.538A2 2 0 0 0 5.219 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zM2 3h3.219l1.072 1H2z"></path><path d="M8.146 6.146a.5.5 0 0 0 0 .707l2 2a.5.5 0 0 0 .707 0l2-2a.5.5 0 1 0-.707-.707L11 7.293V.5a.5.5 0 0 0-1 0v6.793L8.854 6.146a.5.5 0 0 0-.708 0z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 651 B |
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- copied from https://www.svgrepo.com/svg/255881/text -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<g>
|
||||
<g transform="scale(0.03125)">
|
||||
<path d="M405.787,43.574H8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83c0,4.512,3.657,8.17,8.17,8.17h32.681
|
||||
c4.513,0,8.17-3.658,8.17-8.17v-24.511h95.319v119.83c0,4.512,3.657,8.17,8.17,8.17c4.513,0,8.17-3.658,8.17-8.17v-128
|
||||
c0-4.512-3.657-8.17-8.17-8.17H40.851c-4.513,0-8.17,3.658-8.17,8.17v24.511H16.34V59.915h381.277v103.489h-16.34v-24.511
|
||||
c0-4.512-3.657-8.17-8.17-8.17h-111.66c-4.513,0-8.17,3.658-8.17,8.17v288.681c0,4.512,3.657,8.17,8.17,8.17h57.191v16.34H95.319
|
||||
v-16.34h57.191c4.513,0,8.17-3.658,8.17-8.17v-128c0-4.512-3.657-8.17-8.17-8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83H87.149
|
||||
c-4.513,0-8.17,3.658-8.17,8.17v32.681c0,4.512,3.657,8.17,8.17,8.17h239.66c4.513,0,8.17-3.658,8.17-8.17v-32.681
|
||||
c0-4.512-3.657-8.17-8.17-8.17h-57.192v-272.34h95.319v24.511c0,4.512,3.657,8.17,8.17,8.17h32.681c4.513,0,8.17-3.658,8.17-8.17
|
||||
V51.745C413.957,47.233,410.3,43.574,405.787,43.574z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g transform="scale(0.03125)">
|
||||
<path d="M503.83,452.085h-24.511V59.915h24.511c4.513,0,8.17-3.658,8.17-8.17s-3.657-8.17-8.17-8.17h-65.362
|
||||
c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17h24.511v392.17h-24.511c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17
|
||||
h65.362c4.513,0,8.17-3.658,8.17-8.17S508.343,452.085,503.83,452.085z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 16 16">
|
||||
<g>
|
||||
<g transform="scale(0.03125)">
|
||||
<path d="m455.1,137.9l-32.4,32.4-81-81.1 32.4-32.4c6.6-6.6 18.1-6.6 24.7,0l56.3,56.4c6.8,6.8 6.8,17.9 0,24.7zm-270.7,271l-81-81.1 209.4-209.7 81,81.1-209.4,209.7zm-99.7-42l60.6,60.7-84.4,23.8 23.8-84.5zm399.3-282.6l-56.3-56.4c-11-11-50.7-31.8-82.4,0l-285.3,285.5c-2.5,2.5-4.3,5.5-5.2,8.9l-43,153.1c-2,7.1 0.1,14.7 5.2,20 5.2,5.3 15.6,6.2 20,5.2l153-43.1c3.4-0.9 6.4-2.7 8.9-5.2l285.1-285.5c22.7-22.7 22.7-59.7 0-82.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 804 B |
@@ -1 +0,0 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="rgba(255,255,255,1)"><path d="M8 11a1 1 0 01-.707-.293l-2.99-2.99c-.91-.942.471-2.324 1.414-1.414L8 8.586l2.283-2.283c.943-.91 2.324.472 1.414 1.414l-2.99 2.99A1 1 0 018 11z"/></svg>
|
||||
|
Before Width: | Height: | Size: 251 B |
@@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M14.859 3.2a1.335 1.335 0 0 1-1.217.8H13v1h1v8H2V5h8V4h-.642a1.365 1.365 0 0 1-1.325-1.11L6.584 1.538A2 2 0 0 0 5.219 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-1.141-1.8zM2 3h3.219l1.072 1H2zm7.854-.146L11 1.707V8.5a.5.5 0 0 0 1 0V1.707l1.146 1.146a.5.5 0 1 0 .707-.707l-2-2a.5.5 0 0 0-.707 0l-2 2a.5.5 0 0 0 .707.707z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 686 B |
@@ -1,8 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16
|
||||
16"
|
||||
fill="rgba(255,255,255,1)"><path transform='rotate(90) translate(0, -16)'
|
||||
d="M15.707 7.293l-6-6a1 1 0 0 0-1.414 1.414L12.586 7H1a1 1 0 0 0 0 2h11.586l-4.293
|
||||
4.293a1 1 0 1 0 1.414 1.414l6-6a1 1 0 0 0 0-1.414z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 517 B |
@@ -1,13 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16
|
||||
16"
|
||||
fill="rgba(255,255,255,1)">
|
||||
<path
|
||||
transform='rotate(90) translate(0, -16)'
|
||||
d="M15 7H3.414l4.293-4.293a1 1 0 0
|
||||
0-1.414-1.414l-6 6a1 1 0 0 0 0 1.414l6 6a1 1 0 0 0 1.414-1.414L3.414 9H15a1 1 0 0
|
||||
0 0-2z">
|
||||
</path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 517 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M.5 1H7s0-1 1-1 1 1 1 1h6.5s.5 0 .5.5-.5.5-.5.5H.5S0 2 0 1.5.5 1 .5 1zM1 3h14v7c0 2-1 2-2 2H3c-1 0-2 0-2-2zm5 1v7l6-3.5zM3.72 15.33l.53-2s0-.5.65-.35c.51.13.38.63.38.63l-.53 2s0 .5-.64.35c-.53-.13-.39-.63-.39-.63zM11.24 15.61l-.53-1.99s0-.5.38-.63c.51-.13.64.35.64.35l.53 2s0 .5-.38.63c-.5.13-.64-.35-.65-.35z"/></svg>
|
||||
|
Before Width: | Height: | Size: 417 B |
@@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M14 5h-1V1a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v4H2a2 2 0 0 0-2 2v5h3v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-3h3V7a2 2 0 0 0-2-2zM2.5 8a.5.5 0 1 1 .5-.5.5.5 0 0 1-.5.5zm9.5 7H4v-5h8zm0-10H4V1h8zm-6.5 7h4a.5.5 0 0 0 0-1h-4a.5.5 0 1 0 0 1zm0 2h5a.5.5 0 0 0 0-1h-5a.5.5 0 1 0 0 1z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 610 B |
@@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M15.707 14.293l-4.822-4.822a6.019 6.019 0 1 0-1.414 1.414l4.822 4.822a1 1 0 0 0 1.414-1.414zM6 10a4 4 0 1 1 4-4 4 4 0 0 1-4 4z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 472 B |
@@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M8.707 7.293l-5-5a1 1 0 0 0-1.414 1.414L6.586 8l-4.293 4.293a1 1 0 1 0 1.414 1.414l5-5a1 1 0 0 0 0-1.414zm6 0l-5-5a1 1 0 0 0-1.414 1.414L12.586 8l-4.293 4.293a1 1 0 1 0 1.414 1.414l5-5a1 1 0 0 0 0-1.414z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 549 B |
@@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M3 1h10a3.008 3.008 0 0 1 3 3v8a3.009 3.009 0 0 1-3 3H3a3.005 3.005 0 0 1-3-3V4a3.013 3.013 0 0 1 3-3zm11 11V4a1 1 0 0 0-1-1H8v10h5a1 1 0 0 0 1-1zM2 12a1 1 0 0 0 1 1h4V3H3a1 1 0 0 0-1 1v8z"></path><path d="M3.5 5h2a.5.5 0 0 0 0-1h-2a.5.5 0 0 0 0 1zm0 2h2a.5.5 0 0 0 0-1h-2a.5.5 0 0 0 0 1zm1 2h1a.5.5 0 0 0 0-1h-1a.5.5 0 0 0 0 1z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 674 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M6.2 2s.5-.5 1.06 0c.5.5 0 1 0 1l-4.6 4.61s-2.5 2.5 0 5 5 0 5 0L13.8 6.4s1.6-1.6 0-3.2-3.2 0-3.2 0L5.8 8s-.7.7 0 1.4 1.4 0 1.4 0l3.9-3.9s.6-.5 1 0c.5.5 0 1 0 1l-3.8 4s-1.8 1.8-3.5 0C3 8.7 4.8 7 4.8 7l4.7-4.9s2.7-2.6 5.3 0c2.6 2.6 0 5.3 0 5.3l-6.2 6.3s-3.5 3.5-7 0 0-7 0-7z"/></svg>
|
||||
|
Before Width: | Height: | Size: 380 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4.233 4.233" height="16" width="16" fill="rgba(255,255,255,1)"><path d="M.15 2.992c-.198.1-.2.266-.002.365l1.604.802a.93.93 0 00.729-.001l1.602-.801c.198-.1.197-.264 0-.364l-.695-.348c-1.306.595-2.542 0-2.542 0m-.264.53l.658-.329c.6.252 1.238.244 1.754 0l.659.329-1.536.768zM.15 1.935c-.198.1-.198.265 0 .364l1.604.802a.926.926 0 00.727 0l1.603-.802c.198-.099.198-.264 0-.363l-.694-.35c-1.14.56-2.546.001-2.546.001m-.264.53l.664-.332c.52.266 1.261.235 1.75.002l.659.33-1.537.768zM.15.877c-.198.099-.198.264 0 .363l1.604.802a.926.926 0 00.727 0l1.603-.802c.198-.099.198-.264 0-.363L2.481.075a.926.926 0 00-.727 0zm.43.182L2.117.29l1.538.769-1.538.768z"/></svg>
|
||||
|
Before Width: | Height: | Size: 712 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M14 9H8c-1.3 0-1.3 2 0 2h6c1.3 0 1.3-2 0-2zm0-8H5C3.7 1 3.7 3 5 3h9c1.3 0 1.3-2 0-2zM2 1C1 1 .7 2 1.3 2.7 2 3.3 3 3 3 2c0-.5-.4-1-1-1zm3 8c-1 0-1.3 1-.7 1.7.6.6 1.7.2 1.7-.7 0-.5-.4-1-1-1zM14 5H5C3.6 5 3.6 7 5 7h9c1.3 0 1.3-2 0-2zM2 5c-.9 0-1.4 1-.7 1.7C2 7.3 3 6.9 3 6c0-.6-.5-1-1-1zM14 13H5c-1.3 0-1.3 2 0 2h9c1.3 0 1.3-2 0-2zM2 13c-1 0-1.3 1-.7 1.7.7.6 1.7.2 1.7-.712 0-.5-.4-1-1-1z"/></svg>
|
||||
|
Before Width: | Height: | Size: 493 B |
@@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><g style="--darkreader-inline-fill:rgba(81, 82, 83, 0.8);" data-darkreader-inline-fill=""><rect x="1" y="1" width="6" height="6" rx="1" ry="1"></rect><rect x="9" y="1" width="6" height="6" rx="1" ry="1"></rect><rect x="1" y="9" width="6" height="6" rx="1" ry="1"></rect><rect x="9" y="9" width="6" height="6" rx="1" ry="1"></rect></g></svg>
|
||||
|
Before Width: | Height: | Size: 662 B |
@@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M14 7H9V2a1 1 0 0 0-2 0v5H2a1 1 0 0 0 0 2h5v5a1 1 0 0 0 2 0V9h5a1 1 0 0 0 0-2z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 424 B |
@@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><rect x="2" y="7" width="12" height="2" rx="1"></rect></svg>
|
||||
|
Before Width: | Height: | Size: 382 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M13 9L6 5v8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 120 B |
@@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M10 13l4-7H6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 121 B |
@@ -0,0 +1,29 @@
|
||||
.fontSizeWrapper {
|
||||
position: relative;
|
||||
}
|
||||
.slider {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(0,-50%);
|
||||
width: 90%;
|
||||
height: 60px;
|
||||
background: transparent;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0px 15px 40px #7E6D5766;
|
||||
}
|
||||
.slider label {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
font-family: Open Sans;
|
||||
padding-right: 10px;
|
||||
color: white;
|
||||
}
|
||||
.slider input[type="range"] {
|
||||
width: 80%;
|
||||
height: 5px;
|
||||
background: black;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
// Move advanced search to side-menu
|
||||
$("a[href*='advanced']").parent().insertAfter("#nav_new");
|
||||
$("body").addClass("blur");
|
||||
$("body.stat").addClass("stats");
|
||||
$("body.config").addClass("admin");
|
||||
$("body.uiconfig").addClass("admin");
|
||||
@@ -29,8 +28,8 @@ $("body > div.container-fluid > div > div.col-sm-10 > div.filterheader").attr("s
|
||||
// Back button
|
||||
curHref = window.location.href.split("/");
|
||||
prevHref = document.referrer.split("/");
|
||||
$(".navbar-form.navbar-left")
|
||||
.before('<div class="plexBack"><a href="' + encodeURI(document.referrer) + '"></a></div>');
|
||||
$(".plexBack a").attr('href', encodeURI(document.referrer));
|
||||
|
||||
if (history.length === 1 ||
|
||||
curHref[0] +
|
||||
curHref[1] +
|
||||
@@ -44,14 +43,9 @@ if (history.length === 1 ||
|
||||
|
||||
//Weird missing a after pressing back from edit.
|
||||
setTimeout(function () {
|
||||
if ($(".plexBack a").length < 1) {
|
||||
$(".plexBack").append('<a href="' + encodeURI(document.referrer) + '"></a>');
|
||||
}
|
||||
$(".plexBack a").attr('href', encodeURI(document.referrer));
|
||||
}, 10);
|
||||
|
||||
// Home button
|
||||
$(".plexBack").before('<div class="home-btn"></div>');
|
||||
$("a.navbar-brand").clone().appendTo(".home-btn").empty().removeClass("navbar-brand");
|
||||
/////////////////////////////////
|
||||
// Start of Book Details Work //
|
||||
///////////////////////////////
|
||||
@@ -326,13 +320,8 @@ url = window.location.pathname
|
||||
// Move create shelf
|
||||
$("#nav_createshelf").prependTo(".your-shelves");
|
||||
|
||||
// Create drop-down for profile and move elements to it
|
||||
$("#main-nav")
|
||||
.prepend('<li class="dropdown"><a href="#" class="dropdown-toggle profileDrop" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-user"></span></a><ul class="dropdown-menu profileDropli"></ul></li>');
|
||||
$("#top_user").parent().addClass("dropdown").appendTo(".profileDropli");
|
||||
$("#nav_about").addClass("dropdown").appendTo(".profileDropli");
|
||||
$("#register").parent().addClass("dropdown").appendTo(".profileDropli");
|
||||
$("#logout").parent().addClass("dropdown").appendTo(".profileDropli");
|
||||
// Move About link it the profile dropdown
|
||||
$(".profileDropli #top_user").parent().after($("#nav_about").addClass("dropdown"))
|
||||
|
||||
// Remove the modals except from some areas where they are needed
|
||||
bodyClass = $("body").attr("class").split(" ");
|
||||
@@ -666,7 +655,7 @@ $("#sendbtn").attr({
|
||||
|
||||
$("#sendbtn2").attr({
|
||||
"data-toggle-two": "tooltip",
|
||||
"title": $("#sendbtn2").text(), // "Send to E-Reader",
|
||||
"title": $("#sendbtn2").text(), // "Send to eReader",
|
||||
"data-placement": "bottom",
|
||||
"data-viewport": ".btn-toolbar"
|
||||
})
|
||||
|
||||
@@ -125,7 +125,7 @@ function loadArchiveFormats(formats, cb) {
|
||||
_loaded_archive_formats.push(archive_format);
|
||||
break;
|
||||
case 'zip':
|
||||
loadScript(path + 'jszip.js', checkForLoadDone);
|
||||
loadScript(path + 'jszip.min.js', checkForLoadDone);
|
||||
_loaded_archive_formats.push(archive_format);
|
||||
break;
|
||||
case 'tar':
|
||||
|
||||
@@ -163,6 +163,14 @@ kthoom.ImageFile = function(file) {
|
||||
this.mimeType = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset mime type for special files originating from Apple devices
|
||||
// This folder may contain files having image extensions (for example .jpg) but those files are not actual images
|
||||
// Trying to view these files cause corrupted/empty pages in the comic reader and files should be ignored
|
||||
if (this.filename.indexOf("__MACOSX") !== -1) {
|
||||
this.mimeType = undefined;
|
||||
}
|
||||
|
||||
if ( this.mimeType !== undefined) {
|
||||
this.dataURI = createURLFromArray(file.fileData, this.mimeType);
|
||||
}
|
||||
|
||||
@@ -177,6 +177,9 @@
|
||||
|
||||
whileplaying: function () {
|
||||
|
||||
// get csrf_token
|
||||
let csrf_token = $("input[name='csrf_token']").val();
|
||||
|
||||
|
||||
//This sends a bookmark update to calibreweb every 30 seconds.
|
||||
if (this.progressBuffer == undefined) {
|
||||
@@ -187,7 +190,10 @@
|
||||
|
||||
$.ajax(calibre.bookmarkUrl, {
|
||||
method: "post",
|
||||
data: { bookmark: this.position }
|
||||
data: {
|
||||
csrf_token: csrf_token,
|
||||
bookmark: this.position
|
||||
}
|
||||
}).fail(function (xhr, status, error) {
|
||||
console.error(error);
|
||||
});
|
||||
@@ -313,14 +319,14 @@
|
||||
},
|
||||
|
||||
onstop: function () {
|
||||
|
||||
|
||||
$.ajax(calibre.bookmarkUrl, {
|
||||
method: "post",
|
||||
data: { bookmark: this.position }
|
||||
}).fail(function (xhr, status, error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
|
||||
utils.css.remove(dom.o, 'playing');
|
||||
|
||||
},
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.gl={days:["Domingo","Luns","Martes","Mércores","Xoves","Venres","Sábado"],daysShort:["Dom","Lun","Mar","Mér","Xov","Ven","Sáb"],daysMin:["Do","Lu","Ma","Me","Xo","Ve","Sa"],months:["Xaneiro","Febreiro","Marzo","Abril","Maio","Xuño","Xullo","Agosto","Setembro","Outubro","Novembro","Decembro"],monthsShort:["Xan","Feb","Mar","Abr","Mai","Xun","Xul","Ago","Sep","Out","Nov","Dec"],today:"Hoxe",clear:"Limpar",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);
|
||||
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.id={days:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],daysShort:["Mgu","Sen","Sel","Rab","Kam","Jum","Sab"],daysMin:["Mg","Sn","Sl","Ra","Ka","Ju","Sa"],months:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","November","Desember"],monthsShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Ags","Sep","Okt","Nov","Des"],today:"Hari Ini",clear:"Kosongkan"}}(jQuery);
|
||||
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.no={days:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],daysShort:["søn","man","tir","ons","tor","fre","lør"],daysMin:["sø","ma","ti","on","to","fr","lø"],months:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthsShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],today:"i dag",monthsTitle:"Måneder",clear:"Nullstill",weekStart:1,format:"dd.mm.yyyy"}}(jQuery);
|
||||
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.vi={days:["Chủ nhật","Thứ hai","Thứ ba","Thứ tư","Thứ năm","Thứ sáu","Thứ bảy"],daysShort:["CN","Thứ 2","Thứ 3","Thứ 4","Thứ 5","Thứ 6","Thứ 7"],daysMin:["CN","T2","T3","T4","T5","T6","T7"],months:["Tháng 1","Tháng 2","Tháng 3","Tháng 4","Tháng 5","Tháng 6","Tháng 7","Tháng 8","Tháng 9","Tháng 10","Tháng 11","Tháng 12"],monthsShort:["Th1","Th2","Th3","Th4","Th5","Th6","Th7","Th8","Th9","Th10","Th11","Th12"],today:"Hôm nay",clear:"Xóa",format:"dd/mm/yyyy"}}(jQuery);
|
||||
@@ -0,0 +1,412 @@
|
||||
/*!
|
||||
* TinyMCE Language Pack
|
||||
*
|
||||
* Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
|
||||
* Licensed under the Tiny commercial license. See https://www.tiny.cloud/legal/
|
||||
*/
|
||||
tinymce.addI18n('nb_NO', {
|
||||
"Redo": "Gjør om",
|
||||
"Undo": "Angre",
|
||||
"Cut": "Klipp ut",
|
||||
"Copy": "Kopier",
|
||||
"Paste": "Lim inn",
|
||||
"Select all": "Marker alt",
|
||||
"New document": "Nytt dokument",
|
||||
"Ok": "",
|
||||
"Cancel": "Avbryt",
|
||||
"Visual aids": "Visuelle hjelpemidler",
|
||||
"Bold": "Fet",
|
||||
"Italic": "Kursiv",
|
||||
"Underline": "Understreking",
|
||||
"Strikethrough": "Gjennomstreking",
|
||||
"Superscript": "Hevet skrift",
|
||||
"Subscript": "Senket skrift",
|
||||
"Clear formatting": "Fjern formateringer",
|
||||
"Remove": "",
|
||||
"Align left": "Venstrejuster",
|
||||
"Align center": "Midtstill",
|
||||
"Align right": "Høyrejuster",
|
||||
"No alignment": "",
|
||||
"Justify": "Blokkjuster",
|
||||
"Bullet list": "Punktliste",
|
||||
"Numbered list": "Nummerliste",
|
||||
"Decrease indent": "Reduser innrykk",
|
||||
"Increase indent": "Øk innrykk",
|
||||
"Close": "Lukk",
|
||||
"Formats": "Stiler",
|
||||
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.": "Nettleseren din støtter ikke direkte tilgang til utklippsboken. Bruk istedet tastatursnarveiene Ctrl+X/C/V.",
|
||||
"Headings": "Overskrifter",
|
||||
"Heading 1": "Overskrift 1",
|
||||
"Heading 2": "Overskrift 2",
|
||||
"Heading 3": "Overskrift 3",
|
||||
"Heading 4": "Overskrift 4",
|
||||
"Heading 5": "Overskrift 5",
|
||||
"Heading 6": "Overskrift 6",
|
||||
"Preformatted": "Forhåndsformatert",
|
||||
"Div": "",
|
||||
"Pre": "",
|
||||
"Code": "Kode",
|
||||
"Paragraph": "Avsnitt",
|
||||
"Blockquote": "",
|
||||
"Inline": "Innkapslet",
|
||||
"Blocks": "Blokker",
|
||||
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "Lim inn er nå i ren tekst-modus. Kopiert innhold vil bli limt inn som ren tekst inntil du slår av dette valget.",
|
||||
"Fonts": "Fonter",
|
||||
"Font sizes": "",
|
||||
"Class": "Klasse",
|
||||
"Browse for an image": "Søk etter bilde",
|
||||
"OR": "",
|
||||
"Drop an image here": "Slipp et bilde her",
|
||||
"Upload": "Last opp",
|
||||
"Uploading image": "",
|
||||
"Block": "Blokk",
|
||||
"Align": "Juster",
|
||||
"Default": "Standard",
|
||||
"Circle": "Sirkel",
|
||||
"Disc": "Disk",
|
||||
"Square": "Firkant",
|
||||
"Lower Alpha": "Små bokstaver",
|
||||
"Lower Greek": "Greske minuskler",
|
||||
"Lower Roman": "Små romertall",
|
||||
"Upper Alpha": "Store bokstaver",
|
||||
"Upper Roman": "Store romertall",
|
||||
"Anchor...": "Lenke",
|
||||
"Anchor": "",
|
||||
"Name": "Navn",
|
||||
"ID": "",
|
||||
"ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "",
|
||||
"You have unsaved changes are you sure you want to navigate away?": "Du har ikke arkivert endringene. Vil du fortsette uten å arkivere?",
|
||||
"Restore last draft": "Gjenopprett siste utkast",
|
||||
"Special character...": "Spesialtegn...",
|
||||
"Special Character": "",
|
||||
"Source code": "Kildekode",
|
||||
"Insert/Edit code sample": "Sett inn / endre kodeeksempel",
|
||||
"Language": "Språk",
|
||||
"Code sample...": "Kodeeksempel",
|
||||
"Left to right": "Venstre til høyre",
|
||||
"Right to left": "Høyre til venstre",
|
||||
"Title": "Tittel",
|
||||
"Fullscreen": "Fullskjerm",
|
||||
"Action": "Handling",
|
||||
"Shortcut": "Snarvei",
|
||||
"Help": "Hjelp",
|
||||
"Address": "Adresse",
|
||||
"Focus to menubar": "Fokus på menylinje",
|
||||
"Focus to toolbar": "Fokus på verktøylinje",
|
||||
"Focus to element path": "Fokus på elementsti",
|
||||
"Focus to contextual toolbar": "Fokus på kontekstuell verktøylinje",
|
||||
"Insert link (if link plugin activated)": "Sett inn lenke (dersom lenketillegg er aktivert)",
|
||||
"Save (if save plugin activated)": "Lagre (dersom lagretillegg er aktivert)",
|
||||
"Find (if searchreplace plugin activated)": "Finn (dersom tillegg for søk og erstatt er aktivert)",
|
||||
"Plugins installed ({0}):": "Installerte tillegg ({0}):",
|
||||
"Premium plugins:": "Premiumtillegg:",
|
||||
"Learn more...": "Les mer ...",
|
||||
"You are using {0}": "Du bruker {0}",
|
||||
"Plugins": "Programtillegg",
|
||||
"Handy Shortcuts": "Nyttige snarveier",
|
||||
"Horizontal line": "Horisontal linje",
|
||||
"Insert/edit image": "Sett inn / rediger bilde",
|
||||
"Alternative description": "Alternativ beskrivelse",
|
||||
"Accessibility": "Tilgjengelighet",
|
||||
"Image is decorative": "Bilde er dekorasjon",
|
||||
"Source": "Kilde",
|
||||
"Dimensions": "Størrelser",
|
||||
"Constrain proportions": "Begrens proporsjoner",
|
||||
"General": "Generelt",
|
||||
"Advanced": "Avansert",
|
||||
"Style": "Stil",
|
||||
"Vertical space": "Vertikal avstand",
|
||||
"Horizontal space": "Horisontal avstand",
|
||||
"Border": "Ramme",
|
||||
"Insert image": "Sett inn bilde",
|
||||
"Image...": "Bilde...",
|
||||
"Image list": "Bildeliste",
|
||||
"Resize": "Skaler",
|
||||
"Insert date/time": "Sett inn dato/tid",
|
||||
"Date/time": "Dato/tid",
|
||||
"Insert/edit link": "Sett inn / rediger lenke",
|
||||
"Text to display": "Tekst som skal vises",
|
||||
"Url": "",
|
||||
"Open link in...": "Åpne lenke i..",
|
||||
"Current window": "Nåværende vindu",
|
||||
"None": "Ingen",
|
||||
"New window": "Nytt vindu",
|
||||
"Open link": "Åpne lenke",
|
||||
"Remove link": "Fjern lenke",
|
||||
"Anchors": "Forankringspunkter",
|
||||
"Link...": "Lenke...",
|
||||
"Paste or type a link": "Lim inn eller skriv en lenke",
|
||||
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "Oppgitt URL ser ut til å være en e-postadresse. Ønsker du å sette inn påkrevet mailto: prefiks foran e-postadressen?",
|
||||
"The URL you entered seems to be an external link. Do you want to add the required http:// prefix?": "URL du skrev inn ser ut som en ekstern adresse. Vil du legge til det obligatoriske prefikset http://?",
|
||||
"The URL you entered seems to be an external link. Do you want to add the required https:// prefix?": "Nettadressen du fylte inn ser ut til å være en ekstern. Ønsker du å legge til påkrevd 'https://'-prefiks?",
|
||||
"Link list": "Liste over lenker",
|
||||
"Insert video": "Sett inn video",
|
||||
"Insert/edit video": "Sett inn / rediger video",
|
||||
"Insert/edit media": "Sett inn / endre media",
|
||||
"Alternative source": "Alternativ kilde",
|
||||
"Alternative source URL": "Alternativ kilde URL",
|
||||
"Media poster (Image URL)": "Mediaposter (bilde-URL)",
|
||||
"Paste your embed code below:": "Lim inn inkluderingskoden nedenfor:",
|
||||
"Embed": "Inkluder",
|
||||
"Media...": "Media..",
|
||||
"Nonbreaking space": "Hardt mellomrom",
|
||||
"Page break": "Sideskifte",
|
||||
"Paste as text": "Lim inn som tekst",
|
||||
"Preview": "Forhåndsvis",
|
||||
"Print": "",
|
||||
"Print...": "Skriv ut...",
|
||||
"Save": "Lagre",
|
||||
"Find": "Søk etter",
|
||||
"Replace with": "Erstatt med",
|
||||
"Replace": "Erstatt",
|
||||
"Replace all": "Erstatt alle",
|
||||
"Previous": "Forrige",
|
||||
"Next": "Neste",
|
||||
"Find and Replace": "Finn og erstatt",
|
||||
"Find and replace...": "Finn og erstatt...",
|
||||
"Could not find the specified string.": "Kunne ikke finne den spesifiserte teksten",
|
||||
"Match case": "Skill mellom store / små bokstaver",
|
||||
"Find whole words only": "Finn kun hele ord",
|
||||
"Find in selection": "Finn i utvalg",
|
||||
"Insert table": "Sett inn tabell",
|
||||
"Table properties": "Tabellegenskaper",
|
||||
"Delete table": "Slett tabell",
|
||||
"Cell": "Celle",
|
||||
"Row": "Rad",
|
||||
"Column": "Kolonne",
|
||||
"Cell properties": "Celleegenskaper",
|
||||
"Merge cells": "Slå sammen celler",
|
||||
"Split cell": "Splitt celle",
|
||||
"Insert row before": "Sett inn rad før",
|
||||
"Insert row after": "Sett inn rad etter",
|
||||
"Delete row": "Slett rad",
|
||||
"Row properties": "Radegenskaper",
|
||||
"Cut row": "Klipp ut rad",
|
||||
"Cut column": "",
|
||||
"Copy row": "Kopier rad",
|
||||
"Copy column": "",
|
||||
"Paste row before": "Lim inn rad før",
|
||||
"Paste column before": "",
|
||||
"Paste row after": "Lim inn rad etter",
|
||||
"Paste column after": "",
|
||||
"Insert column before": "Sett inn kolonne før",
|
||||
"Insert column after": "Sett inn kolonne etter",
|
||||
"Delete column": "Slett kolonne",
|
||||
"Cols": "Kolonner",
|
||||
"Rows": "Rader",
|
||||
"Width": "Bredde",
|
||||
"Height": "Høyde",
|
||||
"Cell spacing": "Celleavstand",
|
||||
"Cell padding": "Cellemarg",
|
||||
"Row clipboard actions": "",
|
||||
"Column clipboard actions": "",
|
||||
"Table styles": "",
|
||||
"Cell styles": "",
|
||||
"Column header": "",
|
||||
"Row header": "",
|
||||
"Table caption": "",
|
||||
"Caption": "Bildetekst",
|
||||
"Show caption": "Vis bildetekst",
|
||||
"Left": "Venstre",
|
||||
"Center": "Senter",
|
||||
"Right": "Høyre",
|
||||
"Cell type": "Celletype",
|
||||
"Scope": "Omfang",
|
||||
"Alignment": "Justering",
|
||||
"Horizontal align": "",
|
||||
"Vertical align": "",
|
||||
"Top": "Topp",
|
||||
"Middle": "Sentrert",
|
||||
"Bottom": "Bunn",
|
||||
"Header cell": "Overskriftscelle",
|
||||
"Row group": "Radgruppe",
|
||||
"Column group": "Kolonnegruppe",
|
||||
"Row type": "Radtype",
|
||||
"Header": "",
|
||||
"Body": "Brødtekst",
|
||||
"Footer": "Bunntekst",
|
||||
"Border color": "Rammefarge",
|
||||
"Solid": "",
|
||||
"Dotted": "",
|
||||
"Dashed": "",
|
||||
"Double": "",
|
||||
"Groove": "",
|
||||
"Ridge": "",
|
||||
"Inset": "",
|
||||
"Outset": "",
|
||||
"Hidden": "",
|
||||
"Insert template...": "Sett inn mal..",
|
||||
"Templates": "Maler",
|
||||
"Template": "Mal",
|
||||
"Insert Template": "",
|
||||
"Text color": "Tekstfarge",
|
||||
"Background color": "Bakgrunnsfarge",
|
||||
"Custom...": "Tilpasset...",
|
||||
"Custom color": "Tilpasset farge",
|
||||
"No color": "Ingen farge",
|
||||
"Remove color": "Fjern farge",
|
||||
"Show blocks": "Vis blokker",
|
||||
"Show invisible characters": "Vis skjulte tegn",
|
||||
"Word count": "Ordtelling",
|
||||
"Count": "Opptelling",
|
||||
"Document": "Dokument",
|
||||
"Selection": "Utvalg",
|
||||
"Words": "Ord",
|
||||
"Words: {0}": "Ord: {0}",
|
||||
"{0} words": "{0} ord",
|
||||
"File": "Fil",
|
||||
"Edit": "Rediger",
|
||||
"Insert": "Sett inn",
|
||||
"View": "Vis",
|
||||
"Format": "",
|
||||
"Table": "Tabell",
|
||||
"Tools": "Verktøy",
|
||||
"Powered by {0}": "Drevet av {0}",
|
||||
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Tekstredigering. Tast ALT-F9 for meny. Tast ALT-F10 for verktøylinje. Tast ALT-0 for hjelp.",
|
||||
"Image title": "Bildetittel",
|
||||
"Border width": "Bordbredde",
|
||||
"Border style": "Bordstil",
|
||||
"Error": "Feil",
|
||||
"Warn": "Advarsel",
|
||||
"Valid": "Gyldig",
|
||||
"To open the popup, press Shift+Enter": "For å åpne popup, trykk Shift+Enter",
|
||||
"Rich Text Area": "",
|
||||
"Rich Text Area. Press ALT-0 for help.": "Rik-tekstområde. Trykk ALT-0 for hjelp.",
|
||||
"System Font": "Systemfont",
|
||||
"Failed to upload image: {0}": "Opplasting av bilde feilet: {0}",
|
||||
"Failed to load plugin: {0} from url {1}": "Kunne ikke laste tillegg: {0} from url {1}",
|
||||
"Failed to load plugin url: {0}": "Kunne ikke laste tillegg url: {0}",
|
||||
"Failed to initialize plugin: {0}": "Kunne ikke initialisere tillegg: {0}",
|
||||
"example": "eksempel",
|
||||
"Search": "Søk",
|
||||
"All": "Alle",
|
||||
"Currency": "Valuta",
|
||||
"Text": "Tekst",
|
||||
"Quotations": "Sitater",
|
||||
"Mathematical": "Matematisk",
|
||||
"Extended Latin": "Utvidet latin",
|
||||
"Symbols": "Symboler",
|
||||
"Arrows": "Piler",
|
||||
"User Defined": "Brukerdefinert",
|
||||
"dollar sign": "dollartegn",
|
||||
"currency sign": "valutasymbol",
|
||||
"euro-currency sign": "Euro-valutasymbol",
|
||||
"colon sign": "kolon-symbol",
|
||||
"cruzeiro sign": "cruzeiro-symbol",
|
||||
"french franc sign": "franske franc-symbol",
|
||||
"lira sign": "lire-symbol",
|
||||
"mill sign": "mill-symbol",
|
||||
"naira sign": "naira-symbol",
|
||||
"peseta sign": "peseta-symbol",
|
||||
"rupee sign": "rupee-symbol",
|
||||
"won sign": "won-symbol",
|
||||
"new sheqel sign": "Ny sheqel-symbol",
|
||||
"dong sign": "dong-symbol",
|
||||
"kip sign": "kip-symbol",
|
||||
"tugrik sign": "tugrik-symbol",
|
||||
"drachma sign": "drachma-symbol",
|
||||
"german penny symbol": "tysk penny-symbol",
|
||||
"peso sign": "peso-symbol",
|
||||
"guarani sign": "quarani-symbol",
|
||||
"austral sign": "austral-symbol",
|
||||
"hryvnia sign": "hryvina-symbol",
|
||||
"cedi sign": "credi-symbol",
|
||||
"livre tournois sign": "livre tournois-symbol",
|
||||
"spesmilo sign": "spesmilo-symbol",
|
||||
"tenge sign": "tenge-symbol",
|
||||
"indian rupee sign": "indisk rupee-symbol",
|
||||
"turkish lira sign": "tyrkisk lire-symbol",
|
||||
"nordic mark sign": "nordisk mark-symbol",
|
||||
"manat sign": "manat-symbol",
|
||||
"ruble sign": "ruble-symbol",
|
||||
"yen character": "yen-symbol",
|
||||
"yuan character": "yuan-symbol",
|
||||
"yuan character, in hong kong and taiwan": "yuan-symbol, i Hongkong og Taiwan",
|
||||
"yen/yuan character variant one": "yen/yuan-symbol variant en",
|
||||
"Emojis": "",
|
||||
"Emojis...": "",
|
||||
"Loading emojis...": "",
|
||||
"Could not load emojis": "",
|
||||
"People": "Mennesker",
|
||||
"Animals and Nature": "Dyr og natur",
|
||||
"Food and Drink": "Mat og drikke",
|
||||
"Activity": "Aktivitet",
|
||||
"Travel and Places": "Reise og steder",
|
||||
"Objects": "Objekter",
|
||||
"Flags": "Flagg",
|
||||
"Characters": "Tegn",
|
||||
"Characters (no spaces)": "Tegn (uten mellomrom)",
|
||||
"{0} characters": "{0} tegn",
|
||||
"Error: Form submit field collision.": "Feil: Skjemafelt innsendingskollisjon.",
|
||||
"Error: No form element found.": "Feil: Intet skjemafelt funnet.",
|
||||
"Color swatch": "Fargepalett",
|
||||
"Color Picker": "Fargevelger",
|
||||
"Invalid hex color code: {0}": "",
|
||||
"Invalid input": "",
|
||||
"R": "",
|
||||
"Red component": "",
|
||||
"G": "",
|
||||
"Green component": "",
|
||||
"B": "",
|
||||
"Blue component": "",
|
||||
"#": "",
|
||||
"Hex color code": "",
|
||||
"Range 0 to 255": "",
|
||||
"Turquoise": "Turkis",
|
||||
"Green": "Grønn",
|
||||
"Blue": "Blå",
|
||||
"Purple": "Lilla",
|
||||
"Navy Blue": "Marineblå",
|
||||
"Dark Turquoise": "Mørk turkis",
|
||||
"Dark Green": "Mørkegrønn",
|
||||
"Medium Blue": "Mellomblå",
|
||||
"Medium Purple": "Medium lilla",
|
||||
"Midnight Blue": "Midnattblå",
|
||||
"Yellow": "Gul",
|
||||
"Orange": "Oransje",
|
||||
"Red": "Rød",
|
||||
"Light Gray": "Lys grå",
|
||||
"Gray": "Grå",
|
||||
"Dark Yellow": "Mørk gul",
|
||||
"Dark Orange": "Mørk oransje",
|
||||
"Dark Red": "Mørkerød",
|
||||
"Medium Gray": "Medium grå",
|
||||
"Dark Gray": "Mørk grå",
|
||||
"Light Green": "Lys grønn",
|
||||
"Light Yellow": "Lys gul",
|
||||
"Light Red": "Lys rød",
|
||||
"Light Purple": "Lys lilla",
|
||||
"Light Blue": "Lys blå",
|
||||
"Dark Purple": "Mørk lilla",
|
||||
"Dark Blue": "Mørk blå",
|
||||
"Black": "Svart",
|
||||
"White": "Hvit",
|
||||
"Switch to or from fullscreen mode": "Bytt til eller fra fullskjermmodus",
|
||||
"Open help dialog": "Åpne hjelp-dialog",
|
||||
"history": "historikk",
|
||||
"styles": "stiler",
|
||||
"formatting": "formatering",
|
||||
"alignment": "justering",
|
||||
"indentation": "innrykk",
|
||||
"Font": "Skrift",
|
||||
"Size": "Størrelse",
|
||||
"More...": "Mer...",
|
||||
"Select...": "Velg...",
|
||||
"Preferences": "Innstillinger",
|
||||
"Yes": "Ja",
|
||||
"No": "Nei",
|
||||
"Keyboard Navigation": "Navigering med tastaturet",
|
||||
"Version": "Versjon",
|
||||
"Code view": "Kodevisning",
|
||||
"Open popup menu for split buttons": "Åpne sprettoppmeny for splitt-knapper",
|
||||
"List Properties": "Listeegenskaper",
|
||||
"List properties...": "Listeegenskaper ...",
|
||||
"Start list at number": "Start liste på nummer",
|
||||
"Line height": "Linjehøyde",
|
||||
"Dropped file type is not supported": "",
|
||||
"Loading...": "",
|
||||
"ImageProxy HTTP error: Rejected request": "",
|
||||
"ImageProxy HTTP error: Could not find Image Proxy": "",
|
||||
"ImageProxy HTTP error: Incorrect Image Proxy URL": "",
|
||||
"ImageProxy HTTP error: Unknown ImageProxy error": ""
|
||||
});
|
||||
@@ -503,6 +503,23 @@ $(function() {
|
||||
}
|
||||
});
|
||||
});
|
||||
$("#metadata_backup").click(function() {
|
||||
$("#DialogHeader").addClass("hidden");
|
||||
$("#DialogFinished").addClass("hidden");
|
||||
$("#DialogContent").html("");
|
||||
$("#spinner2").show();
|
||||
$.ajax({
|
||||
method: "post",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
url: getPath() + "/metadata_backup",
|
||||
success: function success(data) {
|
||||
$("#spinner2").hide();
|
||||
$("#DialogContent").html(data.text);
|
||||
$("#DialogFinished").removeClass("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
$("#perform_update").click(function() {
|
||||
$("#DialogHeader").removeClass("hidden");
|
||||
$("#spinner2").show();
|
||||
|
||||
@@ -548,12 +548,6 @@ $(function() {
|
||||
},
|
||||
});
|
||||
|
||||
$("#user-table").on("click-cell.bs.table", function (field, value, row, $element) {
|
||||
if (value === "denied_column_value") {
|
||||
confirmDialog("btndeluser", "GeneralDeleteModal", $element.id, user_handle);
|
||||
}
|
||||
});
|
||||
|
||||
$("#user-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table",
|
||||
function (e, rowsAfter, rowsBefore) {
|
||||
var rows = rowsAfter;
|
||||
|
||||
@@ -48,16 +48,12 @@ bookmark_label=Neno ma kombedi
|
||||
tools.title=Gintic
|
||||
tools_label=Gintic
|
||||
first_page.title=Cit i pot buk mukwongo
|
||||
first_page.label=Cit i pot buk mukwongo
|
||||
first_page_label=Cit i pot buk mukwongo
|
||||
last_page.title=Cit i pot buk magiko
|
||||
last_page.label=Cit i pot buk magiko
|
||||
last_page_label=Cit i pot buk magiko
|
||||
page_rotate_cw.title=Wire i tung lacuc
|
||||
page_rotate_cw.label=Wire i tung lacuc
|
||||
page_rotate_cw_label=Wire i tung lacuc
|
||||
page_rotate_ccw.title=Wire i tung lacam
|
||||
page_rotate_ccw.label=Wire i tung lacam
|
||||
page_rotate_ccw_label=Wire i tung lacam
|
||||
|
||||
cursor_text_select_tool.title=Cak gitic me yero coc
|
||||
@@ -124,7 +120,6 @@ print_progress_close=Juki
|
||||
# (the _label strings are alt text for the buttons, the .title strings are
|
||||
# tooltips)
|
||||
toggle_sidebar.title=Lok gintic ma inget
|
||||
toggle_sidebar_notification.title=Lok lanyut me nget (wiyewiye tye i gin acoya/attachments)
|
||||
toggle_sidebar_label=Lok gintic ma inget
|
||||
document_outline.title=Nyut Wiyewiye me Gin acoya (dii-kiryo me yaro/kano jami weng)
|
||||
document_outline_label=Pek pa gin acoya
|
||||
@@ -184,8 +179,6 @@ page_scale_actual=Dite kikome
|
||||
# numerical scale value.
|
||||
page_scale_percent={{scale}}%
|
||||
|
||||
# Loading indicator messages
|
||||
loading_error_indicator=Bal
|
||||
loading_error=Bal otime kun cano PDF.
|
||||
invalid_file_error=Pwail me PDF ma pe atir onyo obale woko.
|
||||
missing_file_error=Pwail me PDF tye ka rem.
|
||||
|
||||
@@ -48,16 +48,12 @@ bookmark_label=Huidige aansig
|
||||
tools.title=Nutsgoed
|
||||
tools_label=Nutsgoed
|
||||
first_page.title=Gaan na eerste bladsy
|
||||
first_page.label=Gaan na eerste bladsy
|
||||
first_page_label=Gaan na eerste bladsy
|
||||
last_page.title=Gaan na laaste bladsy
|
||||
last_page.label=Gaan na laaste bladsy
|
||||
last_page_label=Gaan na laaste bladsy
|
||||
page_rotate_cw.title=Roteer kloksgewys
|
||||
page_rotate_cw.label=Roteer kloksgewys
|
||||
page_rotate_cw_label=Roteer kloksgewys
|
||||
page_rotate_ccw.title=Roteer anti-kloksgewys
|
||||
page_rotate_ccw.label=Roteer anti-kloksgewys
|
||||
page_rotate_ccw_label=Roteer anti-kloksgewys
|
||||
|
||||
cursor_text_select_tool.title=Aktiveer gereedskap om teks te merk
|
||||
@@ -101,7 +97,6 @@ print_progress_close=Kanselleer
|
||||
# (the _label strings are alt text for the buttons, the .title strings are
|
||||
# tooltips)
|
||||
toggle_sidebar.title=Sypaneel aan/af
|
||||
toggle_sidebar_notification.title=Sypaneel aan/af (dokument bevat skema/aanhegsels)
|
||||
toggle_sidebar_label=Sypaneel aan/af
|
||||
document_outline.title=Wys dokumentskema (dubbelklik om alle items oop/toe te vou)
|
||||
document_outline_label=Dokumentoorsig
|
||||
@@ -161,8 +156,6 @@ page_scale_actual=Werklike grootte
|
||||
# numerical scale value.
|
||||
page_scale_percent={{scale}}%
|
||||
|
||||
# Loading indicator messages
|
||||
loading_error_indicator=Fout
|
||||
loading_error='n Fout het voorgekom met die laai van die PDF.
|
||||
invalid_file_error=Ongeldige of korrupte PDF-lêer.
|
||||
missing_file_error=PDF-lêer is weg.
|
||||
|
||||
@@ -42,22 +42,18 @@ print_label=Imprentar
|
||||
download.title=Descargar
|
||||
download_label=Descargar
|
||||
bookmark.title=Vista actual (copiar u ubrir en una nueva finestra)
|
||||
bookmark_label=Anvista actual
|
||||
bookmark_label=Vista actual
|
||||
|
||||
# Secondary toolbar and context menu
|
||||
tools.title=Ferramientas
|
||||
tools_label=Ferramientas
|
||||
first_page.title=Ir ta la primer pachina
|
||||
first_page.label=Ir ta la primer pachina
|
||||
first_page_label=Ir ta la primer pachina
|
||||
last_page.title=Ir ta la zaguer pachina
|
||||
last_page.label=Ir ta la zaguera pachina
|
||||
last_page_label=Ir ta la zaguer pachina
|
||||
page_rotate_cw.title=Chirar enta la dreita
|
||||
page_rotate_cw.label=Chirar enta la dreita
|
||||
page_rotate_cw_label=Chira enta la dreita
|
||||
page_rotate_ccw.title=Chirar enta la zurda
|
||||
page_rotate_ccw.label=Chirar en sentiu antihorario
|
||||
page_rotate_ccw_label=Chirar enta la zurda
|
||||
|
||||
cursor_text_select_tool.title=Activar la ferramienta de selección de texto
|
||||
@@ -65,6 +61,20 @@ cursor_text_select_tool_label=Ferramienta de selección de texto
|
||||
cursor_hand_tool.title=Activar la ferramienta man
|
||||
cursor_hand_tool_label=Ferramienta man
|
||||
|
||||
scroll_vertical.title=Usar lo desplazamiento vertical
|
||||
scroll_vertical_label=Desplazamiento vertical
|
||||
scroll_horizontal.title=Usar lo desplazamiento horizontal
|
||||
scroll_horizontal_label=Desplazamiento horizontal
|
||||
scroll_wrapped.title=Activaar lo desplazamiento contino
|
||||
scroll_wrapped_label=Desplazamiento contino
|
||||
|
||||
spread_none.title=No unir vistas de pachinas
|
||||
spread_none_label=Una pachina nomás
|
||||
spread_odd.title=Mostrar vista de pachinas, con as impars a la zurda
|
||||
spread_odd_label=Doble pachina, impar a la zurda
|
||||
spread_even.title=Amostrar vista de pachinas, con as pars a la zurda
|
||||
spread_even_label=Doble pachina, para a la zurda
|
||||
|
||||
# Document properties dialog box
|
||||
document_properties.title=Propiedatz d'o documento...
|
||||
document_properties_label=Propiedatz d'o documento...
|
||||
@@ -89,6 +99,28 @@ document_properties_creator=Creyador:
|
||||
document_properties_producer=Creyador de PDF:
|
||||
document_properties_version=Versión de PDF:
|
||||
document_properties_page_count=Numero de pachinas:
|
||||
document_properties_page_size=Mida de pachina:
|
||||
document_properties_page_size_unit_inches=pulgadas
|
||||
document_properties_page_size_unit_millimeters=mm
|
||||
document_properties_page_size_orientation_portrait=vertical
|
||||
document_properties_page_size_orientation_landscape=horizontal
|
||||
document_properties_page_size_name_a3=A3
|
||||
document_properties_page_size_name_a4=A4
|
||||
document_properties_page_size_name_letter=Carta
|
||||
document_properties_page_size_name_legal=Legal
|
||||
# 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}} x {{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}} x {{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=Vista web rapida:
|
||||
document_properties_linearized_yes=Sí
|
||||
document_properties_linearized_no=No
|
||||
document_properties_close=Zarrar
|
||||
|
||||
print_progress_message=Se ye preparando la documentación pa imprentar…
|
||||
@@ -101,17 +133,20 @@ print_progress_close=Cancelar
|
||||
# (the _label strings are alt text for the buttons, the .title strings are
|
||||
# tooltips)
|
||||
toggle_sidebar.title=Amostrar u amagar a barra lateral
|
||||
toggle_sidebar_notification.title=Cambiar barra lateral (lo documento contiene esquema/adchuntos)
|
||||
toggle_sidebar_notification2.title=Cambiar barra lateral (lo documento contiene esquema/adchuntos/capas)
|
||||
toggle_sidebar_label=Amostrar a barra lateral
|
||||
document_outline.title=Amostrar esquema d'o documento (fer doble clic pa expandir/compactar totz los items)
|
||||
document_outline_label=Esquema d'o documento
|
||||
attachments.title=Amostrar os adchuntos
|
||||
attachments_label=Adchuntos
|
||||
layers.title=Amostrar capas (doble clic para reiniciar totas las capas a lo estau per defecto)
|
||||
layers_label=Capas
|
||||
thumbs.title=Amostrar as miniaturas
|
||||
thumbs_label=Miniaturas
|
||||
findbar.title=Trobar en o documento
|
||||
findbar_label=Trobar
|
||||
|
||||
additional_layers=Capas adicionals
|
||||
# Thumbnails panel item (tooltip and alt text for images)
|
||||
# LOCALIZATION NOTE (thumb_page_title): "{{page}}" will be replaced by the page
|
||||
# number.
|
||||
@@ -129,8 +164,30 @@ find_next.title=Trobar a siguient coincidencia d'a frase
|
||||
find_next_label=Siguient
|
||||
find_highlight=Resaltar-lo tot
|
||||
find_match_case_label=Coincidencia de mayusclas/minusclas
|
||||
find_entire_word_label=Parolas completas
|
||||
find_reached_top=S'ha plegau a l'inicio d'o documento, se contina dende baixo
|
||||
find_reached_bottom=S'ha plegau a la fin d'o documento, se contina dende alto
|
||||
# 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]={{current}} de {{total}} coincidencia
|
||||
find_match_count[two]={{current}} de {{total}} coincidencias
|
||||
find_match_count[few]={{current}} de {{total}} coincidencias
|
||||
find_match_count[many]={{current}} de {{total}} coincidencias
|
||||
find_match_count[other]={{current}} de {{total}} coincidencias
|
||||
# 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]=Mas de {{limit}} coincidencias
|
||||
find_match_count_limit[one]=Mas de {{limit}} coincidencias
|
||||
find_match_count_limit[two]=Mas que {{limit}} coincidencias
|
||||
find_match_count_limit[few]=Mas que {{limit}} coincidencias
|
||||
find_match_count_limit[many]=Mas que {{limit}} coincidencias
|
||||
find_match_count_limit[other]=Mas que {{limit}} coincidencias
|
||||
find_not_found=No s'ha trobau a frase
|
||||
|
||||
# Error panel labels
|
||||
@@ -161,13 +218,15 @@ page_scale_actual=Grandaria actual
|
||||
# numerical scale value.
|
||||
page_scale_percent={{scale}}%
|
||||
|
||||
# Loading indicator messages
|
||||
loading_error_indicator=Error
|
||||
loading_error=S'ha produciu una error en cargar o PDF.
|
||||
invalid_file_error=O PDF no ye valido u ye estorbau.
|
||||
missing_file_error=No i ha fichero PDF.
|
||||
unexpected_response_error=Respuesta a lo servicio inasperada.
|
||||
|
||||
# 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).
|
||||
|
||||
@@ -47,17 +47,13 @@ bookmark_label=المنظور الحالي
|
||||
# Secondary toolbar and context menu
|
||||
tools.title=الأدوات
|
||||
tools_label=الأدوات
|
||||
first_page.title=اذهب إلى الصفحة الأولى
|
||||
first_page.label=اذهب إلى الصفحة الأولى
|
||||
first_page_label=اذهب إلى الصفحة الأولى
|
||||
last_page.title=اذهب إلى الصفحة الأخيرة
|
||||
last_page.label=اذهب إلى الصفحة الأخيرة
|
||||
last_page_label=اذهب إلى الصفحة الأخيرة
|
||||
first_page.title=انتقل إلى الصفحة الأولى
|
||||
first_page_label=انتقل إلى الصفحة الأولى
|
||||
last_page.title=انتقل إلى الصفحة الأخيرة
|
||||
last_page_label=انتقل إلى الصفحة الأخيرة
|
||||
page_rotate_cw.title=أدر باتجاه عقارب الساعة
|
||||
page_rotate_cw.label=أدر باتجاه عقارب الساعة
|
||||
page_rotate_cw_label=أدر باتجاه عقارب الساعة
|
||||
page_rotate_ccw.title=أدر بعكس اتجاه عقارب الساعة
|
||||
page_rotate_ccw.label=أدر بعكس اتجاه عقارب الساعة
|
||||
page_rotate_ccw_label=أدر بعكس اتجاه عقارب الساعة
|
||||
|
||||
cursor_text_select_tool.title=فعّل أداة اختيار النص
|
||||
@@ -137,17 +133,22 @@ print_progress_close=ألغِ
|
||||
# (the _label strings are alt text for the buttons, the .title strings are
|
||||
# tooltips)
|
||||
toggle_sidebar.title=بدّل ظهور الشريط الجانبي
|
||||
toggle_sidebar_notification.title=بدّل ظهور الشريط الجانبي (يحتوي المستند على مخطط أو مرفقات)
|
||||
toggle_sidebar_notification2.title=بدّل ظهور الشريط الجانبي (يحتوي المستند على مخطط أو مرفقات أو طبقات)
|
||||
toggle_sidebar_label=بدّل ظهور الشريط الجانبي
|
||||
document_outline.title=اعرض فهرس المستند (نقر مزدوج لتمديد أو تقليص كل العناصر)
|
||||
document_outline_label=مخطط المستند
|
||||
attachments.title=اعرض المرفقات
|
||||
attachments_label=المُرفقات
|
||||
layers.title=اعرض الطبقات (انقر مرتين لتصفير كل الطبقات إلى الحالة المبدئية)
|
||||
layers_label=الطبقات
|
||||
thumbs.title=اعرض مُصغرات
|
||||
thumbs_label=مُصغّرات
|
||||
findbar.title=ابحث في المستند
|
||||
findbar_label=ابحث
|
||||
|
||||
additional_layers=الطبقات الإضافية
|
||||
# LOCALIZATION NOTE (page_landmark): "{{page}}" will be replaced by the page number.
|
||||
page_landmark=صفحة {{page}}
|
||||
# Thumbnails panel item (tooltip and alt text for images)
|
||||
# LOCALIZATION NOTE (thumb_page_title): "{{page}}" will be replaced by the page
|
||||
# number.
|
||||
@@ -220,7 +221,7 @@ page_scale_actual=الحجم الفعلي
|
||||
page_scale_percent={{scale}}٪
|
||||
|
||||
# Loading indicator messages
|
||||
loading_error_indicator=عطل
|
||||
loading=يحمّل…
|
||||
loading_error=حدث عطل أثناء تحميل ملف PDF.
|
||||
invalid_file_error=ملف PDF تالف أو غير صحيح.
|
||||
missing_file_error=ملف PDF غير موجود.
|
||||
|
||||
@@ -28,44 +28,51 @@ of_pages=de {{pagesCount}}
|
||||
# respectively a number representing the total number of pages in the document.
|
||||
page_of_pages=({{pageNumber}} de {{pagesCount}})
|
||||
|
||||
zoom_out.title=Reducir
|
||||
zoom_out_label=Reducir
|
||||
zoom_in.title=Aumentar
|
||||
zoom_in_label=Aumentar
|
||||
zoom.title=Tamañu
|
||||
open_file.title=Abrir ficheru
|
||||
zoom_out.title=Alloñar
|
||||
zoom_out_label=Alloña
|
||||
zoom_in.title=Averar
|
||||
zoom_in_label=Avera
|
||||
zoom.title=Zoom
|
||||
presentation_mode.title=Cambiar al mou de presentación
|
||||
presentation_mode_label=Mou de presentación
|
||||
open_file_label=Abrir
|
||||
print.title=Imprentar
|
||||
print_label=Imprentar
|
||||
download.title=Descargar
|
||||
download_label=Descargar
|
||||
bookmark.title=Vista actual (copiar o abrir nuna nueva ventana)
|
||||
download.title=Baxar
|
||||
download_label=Baxar
|
||||
bookmark_label=Vista actual
|
||||
|
||||
# Secondary toolbar and context menu
|
||||
tools.title=Ferramientes
|
||||
tools_label=Ferramientes
|
||||
first_page.title=Dir a la primer páxina
|
||||
first_page.label=Dir a la primer páxina
|
||||
first_page_label=Dir a la primer páxina
|
||||
last_page.title=Dir a la postrer páxina
|
||||
last_page.label=Dir a la cabera páxina
|
||||
last_page_label=Dir a la postrer páxina
|
||||
page_rotate_cw.title=Xirar en sen horariu
|
||||
page_rotate_cw_label=Xirar en sen horariu
|
||||
page_rotate_ccw.title=Xirar en sen antihorariu
|
||||
page_rotate_ccw_label=Xirar en sen antihorariu
|
||||
last_page_label=Dir a la última páxina
|
||||
page_rotate_cw.title=Voltia a la derecha
|
||||
page_rotate_cw_label=Voltiar a la derecha
|
||||
page_rotate_ccw.title=Voltia a la esquierda
|
||||
page_rotate_ccw_label=Voltiar a la esquierda
|
||||
|
||||
cursor_text_select_tool.title=Activa la ferramienta d'esbilla de testu
|
||||
cursor_text_select_tool_label=Ferramienta d'esbilla de testu
|
||||
cursor_hand_tool.title=Activa la ferramienta de mano
|
||||
cursor_hand_tool_label=Ferramienta de mano
|
||||
|
||||
scroll_vertical.title=Usa'l desplazamientu vertical
|
||||
scroll_vertical_label=Desplazamientu vertical
|
||||
scroll_horizontal.title=Usa'l desplazamientu horizontal
|
||||
scroll_horizontal_label=Desplazamientu horizontal
|
||||
scroll_wrapped.title=Usa'l desplazamientu continuu
|
||||
scroll_wrapped_label=Desplazamientu continuu
|
||||
|
||||
spread_none_label=Fueyes individuales
|
||||
spread_odd_label=Fueyes pares
|
||||
spread_even_label=Fueyes impares
|
||||
|
||||
# Document properties dialog box
|
||||
document_properties.title=Propiedaes del documentu…
|
||||
document_properties_label=Propiedaes del documentu…
|
||||
document_properties_file_name=Nome de ficheru:
|
||||
document_properties_file_size=Tamañu de ficheru:
|
||||
document_properties_file_name=Nome del ficheru:
|
||||
document_properties_file_size=Tamañu del ficheru:
|
||||
# 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}} bytes)
|
||||
@@ -73,20 +80,20 @@ document_properties_kb={{size_kb}} KB ({{size_b}} bytes)
|
||||
# will be replaced by the PDF file size in megabytes, respectively in bytes.
|
||||
document_properties_mb={{size_mb}} MB ({{size_b}} bytes)
|
||||
document_properties_title=Títulu:
|
||||
document_properties_author=Autor:
|
||||
document_properties_subject=Asuntu:
|
||||
document_properties_keywords=Pallabres clave:
|
||||
document_properties_creation_date=Data de creación:
|
||||
document_properties_modification_date=Data de modificación:
|
||||
# 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=Creador:
|
||||
document_properties_producer=Productor PDF:
|
||||
document_properties_version=Versión PDF:
|
||||
document_properties_producer=Productor del PDF:
|
||||
document_properties_version=Versión del PDF:
|
||||
document_properties_page_count=Númberu de páxines:
|
||||
document_properties_page_size=Tamañu de páxina:
|
||||
document_properties_page_size_unit_inches=in
|
||||
document_properties_page_size_unit_millimeters=mm
|
||||
document_properties_page_size_orientation_portrait=vertical
|
||||
document_properties_page_size_orientation_landscape=horizontal
|
||||
document_properties_page_size_name_a3=A3
|
||||
document_properties_page_size_name_a4=A4
|
||||
# LOCALIZATION NOTE (document_properties_page_size_dimension_string):
|
||||
@@ -99,11 +106,11 @@ 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=Vista web rápida:
|
||||
document_properties_linearized_yes=Sí
|
||||
document_properties_linearized_no=Non
|
||||
document_properties_close=Zarrar
|
||||
|
||||
print_progress_message=Tresnando documentu pa imprentar…
|
||||
# LOCALIZATION NOTE (print_progress_percent): "{{progress}}" will be replaced by
|
||||
# a numerical per cent value.
|
||||
print_progress_percent={{progress}}%
|
||||
@@ -112,54 +119,56 @@ print_progress_close=Encaboxar
|
||||
# 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=Camudar barra llateral
|
||||
toggle_sidebar_label=Camudar barra llateral
|
||||
document_outline.title=Amosar esquema del documentu (duble clic pa espander/contrayer tolos elementos)
|
||||
document_outline_label=Esquema del documentu
|
||||
attachments.title=Amosar axuntos
|
||||
toggle_sidebar.title=Alternar la barra llateral
|
||||
attachments.title=Amosar los axuntos
|
||||
attachments_label=Axuntos
|
||||
thumbs.title=Amosar miniatures
|
||||
layers_label=Capes
|
||||
thumbs.title=Amosar les miniatures
|
||||
thumbs_label=Miniatures
|
||||
findbar.title=Guetar nel documentu
|
||||
findbar_label=Guetar
|
||||
findbar_label=Atopar
|
||||
|
||||
additional_layers=Capes adicionales
|
||||
# LOCALIZATION NOTE (page_landmark): "{{page}}" will be replaced by the page number.
|
||||
page_landmark=Páxina {{page}}
|
||||
# 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=Páxina {{page}}
|
||||
# LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page
|
||||
# number.
|
||||
thumb_page_canvas=Miniatura de la páxina {{page}}
|
||||
|
||||
# Find panel button title and messages
|
||||
find_input.title=Guetar
|
||||
find_input.placeholder=Guetar nel documentu…
|
||||
find_previous.title=Alcontrar l'anterior apaición de la fras
|
||||
find_previous_label=Anterior
|
||||
find_next.title=Alcontrar la siguiente apaición d'esta fras
|
||||
find_next_label=Siguiente
|
||||
find_highlight=Remarcar toos
|
||||
find_match_case_label=Coincidencia de mayús./minús.
|
||||
find_entire_word_label=Pallabres enteres
|
||||
find_reached_top=Algamóse'l principiu del documentu, siguir dende'l final
|
||||
find_reached_bottom=Algamóse'l final del documentu, siguir dende'l principiu
|
||||
find_entire_word_label=Pallabres completes
|
||||
find_reached_top=Algamóse'l comienzu de la páxina, síguese dende abaxo
|
||||
find_reached_bottom=Algamóse la fin del documentu, síguese dende arriba
|
||||
# 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[one]={{current}} de {{total}} coincidencia
|
||||
find_match_count[two]={{current}} de {{total}} coincidencies
|
||||
find_match_count[few]={{current}} de {{total}} coincidencies
|
||||
find_match_count[many]={{current}} de {{total}} coincidencies
|
||||
find_match_count[other]={{current}} de {{total}} coincidencies
|
||||
# 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_not_found=Frase non atopada
|
||||
find_match_count_limit[zero]=Más de {{limit}} coincidencies
|
||||
find_match_count_limit[one]=Más de {{limit}} coincidencia
|
||||
find_match_count_limit[two]=Más de {{limit}} coincidencies
|
||||
find_match_count_limit[few]=Más de {{limit}} coincidencies
|
||||
find_match_count_limit[many]=Más de {{limit}} coincidencies
|
||||
find_match_count_limit[other]=Más de {{limit}} coincidencies
|
||||
|
||||
# Error panel labels
|
||||
error_more_info=Más información
|
||||
error_less_info=Menos información
|
||||
error_close=Zarrar
|
||||
# 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}} (build: {{build}})
|
||||
error_version_info=PDF.js v{{version}} (compilación: {{build}})
|
||||
# LOCALIZATION NOTE (error_message): "{{message}}" will be replaced by an
|
||||
# english string describing the error.
|
||||
error_message=Mensaxe: {{message}}
|
||||
@@ -170,37 +179,29 @@ error_stack=Pila: {{stack}}
|
||||
error_file=Ficheru: {{file}}
|
||||
# LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number
|
||||
error_line=Llinia: {{line}}
|
||||
rendering_error=Hebo un fallu al renderizar la páxina.
|
||||
|
||||
# Predefined zoom values
|
||||
page_scale_width=Anchor de la páxina
|
||||
page_scale_fit=Axuste de la páxina
|
||||
page_scale_auto=Tamañu automáticu
|
||||
page_scale_actual=Tamañu actual
|
||||
page_scale_auto=Zoom automáticu
|
||||
page_scale_actual=Tamañu real
|
||||
# LOCALIZATION NOTE (page_scale_percent): "{{scale}}" will be replaced by a
|
||||
# numerical scale value.
|
||||
page_scale_percent={{scale}}%
|
||||
|
||||
# Loading indicator messages
|
||||
loading_error_indicator=Fallu
|
||||
loading_error=Hebo un fallu al cargar el PDF.
|
||||
invalid_file_error=Ficheru PDF inválidu o corruptu.
|
||||
missing_file_error=Nun hai ficheru PDF.
|
||||
unexpected_response_error=Rempuesta inesperada del sirvidor.
|
||||
loading=Cargando…
|
||||
loading_error=Asocedió un fallu mentanto se cargaba'l PDF.
|
||||
|
||||
# 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=[Anotación {{type}}]
|
||||
password_label=Introduz la contraseña p'abrir esti ficheru PDF
|
||||
password_invalid=Contraseña non válida. Vuelvi a intentalo.
|
||||
password_ok=Aceutar
|
||||
password_ok=Aceptar
|
||||
password_cancel=Encaboxar
|
||||
|
||||
printing_not_supported=Alvertencia: La imprentación entá nun ta sofitada dafechu nesti restolador.
|
||||
printing_not_ready=Avisu: Esti PDF nun se cargó completamente pa poder imprentase.
|
||||
web_fonts_disabled=Les fontes web tán desactivaes: ye imposible usar les fontes PDF embebíes.
|
||||
# LOCALIZATION NOTE (unsupported_feature_signatures): Should contain the same
|
||||
# exact string as in the `chrome.properties` file.
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ open_file.title=Fayl Aç
|
||||
open_file_label=Aç
|
||||
print.title=Yazdır
|
||||
print_label=Yazdır
|
||||
download.title=Yüklə
|
||||
download_label=Yüklə
|
||||
download.title=Endir
|
||||
download_label=Endir
|
||||
bookmark.title=Hazırkı görünüş (köçür və ya yeni pəncərədə aç)
|
||||
bookmark_label=Hazırkı görünüş
|
||||
|
||||
@@ -48,16 +48,12 @@ bookmark_label=Hazırkı görünüş
|
||||
tools.title=Alətlər
|
||||
tools_label=Alətlər
|
||||
first_page.title=İlk Səhifəyə get
|
||||
first_page.label=İlk Səhifəyə get
|
||||
first_page_label=İlk Səhifəyə get
|
||||
last_page.title=Son Səhifəyə get
|
||||
last_page.label=Son Səhifəyə get
|
||||
last_page_label=Son Səhifəyə get
|
||||
page_rotate_cw.title=Saat İstiqamətində Fırlat
|
||||
page_rotate_cw.label=Saat İstiqamətində Fırlat
|
||||
page_rotate_cw_label=Saat İstiqamətində Fırlat
|
||||
page_rotate_ccw.title=Saat İstiqamətinin Əksinə Fırlat
|
||||
page_rotate_ccw.label=Saat İstiqamətinin Əksinə Fırlat
|
||||
page_rotate_ccw_label=Saat İstiqamətinin Əksinə Fırlat
|
||||
|
||||
cursor_text_select_tool.title=Yazı seçmə alətini aktivləşdir
|
||||
@@ -137,19 +133,20 @@ print_progress_close=Ləğv et
|
||||
# (the _label strings are alt text for the buttons, the .title strings are
|
||||
# tooltips)
|
||||
toggle_sidebar.title=Yan Paneli Aç/Bağla
|
||||
toggle_sidebar_notification.title=Yan paneli çevir (sənəddə icmal/bağlama var)
|
||||
toggle_sidebar_notification2.title=Yan paneli çevir (sənəddə icmal/bağlamalar/laylar mövcuddur)
|
||||
toggle_sidebar_label=Yan Paneli Aç/Bağla
|
||||
document_outline.title=Sənədin eskizini göstər (bütün bəndləri açmaq/yığmaq üçün iki dəfə klikləyin)
|
||||
document_outline_label=Sənəd strukturu
|
||||
attachments.title=Bağlamaları göstər
|
||||
attachments_label=Bağlamalar
|
||||
layers.title=Layları göstər (bütün layları ilkin halına sıfırlamaq üçün iki dəfə klikləyin)
|
||||
layers_label=Laylar
|
||||
thumbs.title=Kiçik şəkilləri göstər
|
||||
thumbs_label=Kiçik şəkillər
|
||||
findbar.title=Sənəddə Tap
|
||||
findbar_label=Tap
|
||||
|
||||
# LOCALIZATION NOTE (page_canvas): "{{page}}" will be replaced by the page number.
|
||||
page_canvas=Səhifə {{page}}
|
||||
additional_layers=Əlavə laylar
|
||||
# Thumbnails panel item (tooltip and alt text for images)
|
||||
# LOCALIZATION NOTE (thumb_page_title): "{{page}}" will be replaced by the page
|
||||
# number.
|
||||
@@ -221,8 +218,6 @@ page_scale_actual=Hazırkı Həcm
|
||||
# numerical scale value.
|
||||
page_scale_percent={{scale}}%
|
||||
|
||||
# Loading indicator messages
|
||||
loading_error_indicator=Səhv
|
||||
loading_error=PDF yüklenərkən bir səhv yarandı.
|
||||
invalid_file_error=Səhv və ya zədələnmiş olmuş PDF fayl.
|
||||
missing_file_error=PDF fayl yoxdur.
|
||||
|
||||