This commit is contained in:
cbartondock 2021-06-13 21:38:25 -04:00
commit bf4564e365
93 changed files with 21665 additions and 15642 deletions

View File

@ -2,6 +2,13 @@
Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database.
[![GitHub License](https://img.shields.io/github/license/janeczku/calibre-web?style=flat-square)](https://github.com/janeczku/calibre-web/blob/master/LICENSE)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/janeczku/calibre-web?logo=github&style=flat-square&label=commits)]()
[![GitHub all releases](https://img.shields.io/github/downloads/janeczku/calibre-web/total?logo=github&style=flat-square)](https://github.com/janeczku/calibre-web/releases)
[![PyPI](https://img.shields.io/pypi/v/calibreweb?logo=pypi&logoColor=fff&style=flat-square)](https://pypi.org/project/calibreweb/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/calibreweb?logo=pypi&logoColor=fff&style=flat-square)](https://pypi.org/project/calibreweb/)
[![Discord](https://img.shields.io/discord/838810113564344381?label=Discord&logo=discord&style=flat-square)](https://discord.gg/h2VsJ2NEfB)
*This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.*
![Main screen](https://github.com/janeczku/calibre-web/wiki/images/main_screen.png)
@ -32,12 +39,19 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
## Quick start
#### Install via pip
1. Install calibre web via pip with the command `pip install calibreweb`.
2. Optional features can also be installed via pip, please refer to [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-Windows) for details
3. Calibre-Web can be started afterwards by typing `cps` or `python3 -m cps`
#### Manual installation
1. Install dependencies by running `pip3 install --target vendor -r requirements.txt` (python3.x). Alternativly set up a python virtual environment.
2. Execute the command: `python3 cps.py` (or `nohup python3 cps.py` - recommended if you want to exit the terminal window)
3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration)
5. Go to Login page
Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration)
Go to Login page
**Default admin login:**\
*Username:* admin\
@ -80,7 +94,9 @@ Pre-built Docker images are available in these Docker Hub repositories:
+ The "path to convertertool" should be set to `/usr/bin/ebook-convert`
+ The "path to unrar" should be set to `/usr/bin/unrar`
# Wiki
# Contact
Just reach us out on [Discord](https://discord.gg/h2VsJ2NEfB)
For further information, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki)

View File

@ -83,7 +83,9 @@ log = logger.create()
from . import services
db.CalibreDB.setup_db(config, cli.settingspath)
db.CalibreDB.update_config(config)
db.CalibreDB.setup_db(config.config_calibre_dir, cli.settingspath)
calibre_db = db.CalibreDB()

View File

@ -40,7 +40,7 @@ from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import func, or_, text
from . import constants, logger, helper, services
from .cli import filepicker
# from .cli import filepicker
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \
valid_email, check_username
@ -97,19 +97,6 @@ def admin_required(f):
return inner
def unconfigured(f):
"""
Checks if calibre-web instance is not configured
"""
@wraps(f)
def inner(*args, **kwargs):
if not config.db_configured:
return f(*args, **kwargs)
abort(403)
return inner
@admi.before_app_request
def before_request():
if current_user.is_authenticated:
@ -124,10 +111,14 @@ def before_request():
g.shelves_access = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
if '/static/' not in request.path and not config.db_configured and \
request.endpoint not in ('admin.basic_configuration',
'login',
'admin.config_pathchooser'):
return redirect(url_for('admin.basic_configuration'))
request.endpoint not in ('admin.ajax_db_config',
'admin.simulatedbchange',
'admin.db_configuration',
'web.login',
'web.logout',
'admin.load_dialogtexts',
'admin.ajax_pathchooser'):
return redirect(url_for('admin.db_configuration'))
@admi.route("/admin")
@ -194,16 +185,46 @@ def admin():
feature_support=feature_support, kobo_support=kobo_support,
title=_(u"Admin page"), page="admin")
@admi.route("/admin/dbconfig", methods=["GET", "POST"])
@login_required
@admin_required
def db_configuration():
if request.method == "POST":
return _db_configuration_update_helper()
return _db_configuration_result()
@admi.route("/admin/config", methods=["GET", "POST"])
@admi.route("/admin/config", methods=["GET"])
@login_required
@admin_required
def configuration():
if request.method == "POST":
return _configuration_update_helper(True)
return _configuration_result()
return render_title_template("config_edit.html",
config=config,
provider=oauthblueprints,
feature_support=feature_support,
title=_(u"Basic Configuration"), page="config")
@admi.route("/admin/ajaxconfig", methods=["POST"])
@login_required
@admin_required
def ajax_config():
return _configuration_update_helper()
@admi.route("/admin/ajaxdbconfig", methods=["POST"])
@login_required
@admin_required
def ajax_db_config():
return _db_configuration_update_helper()
@admi.route("/admin/alive", methods=["GET"])
@login_required
@admin_required
def calibreweb_alive():
return "", 200
@admi.route("/admin/viewconfig")
@login_required
@admin_required
@ -236,7 +257,7 @@ def edit_user_table():
custom_values = []
if not config.config_anonbrowse:
allUser = allUser.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS)
kobo_support = feature_support['kobo'] and config.config_kobo_sync
return render_title_template("user_table.html",
users=allUser.all(),
tags=tags,
@ -245,6 +266,7 @@ def edit_user_table():
languages=languages,
visiblility=visibility,
all_roles=constants.ALL_ROLES,
kobo_support=kobo_support,
sidebar_settings=constants.sidebar_settings,
title=_(u"Edit Users"),
page="usertable")
@ -391,6 +413,8 @@ def edit_list_user(param):
user.name = check_username(vals['value'])
elif param =='email':
user.email = check_email(vals['value'])
elif param =='kobo_only_shelves_sync':
user.kobo_only_shelves_sync = int(vals['value'] == 'true')
elif param == 'kindle_mail':
user.kindle_mail = valid_email(vals['value']) if vals['value'] else ""
elif param.endswith('role'):
@ -495,30 +519,30 @@ def check_valid_restricted_column(column):
def update_view_configuration():
to_save = request.form.to_dict()
_config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y)
_config_int = lambda x: config.set_from_dictionary(to_save, x, int)
# _config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y)
# _config_int = lambda x: config.set_from_dictionary(to_save, x, int)
_config_string("config_calibre_web_title")
_config_string("config_columns_to_ignore")
if _config_string("config_title_regex"):
_config_string(to_save, "config_calibre_web_title")
_config_string(to_save, "config_columns_to_ignore")
if _config_string(to_save, "config_title_regex"):
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")
log.debug("Invalid Read column")
return view_configuration()
_config_int("config_read_column")
_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")
log.debug("Invalid Restricted Column")
return view_configuration()
_config_int("config_restricted_column")
_config_int(to_save, "config_restricted_column")
_config_int("config_theme")
_config_int("config_random_books")
_config_int("config_books_per_page")
_config_int("config_authors_max")
_config_int(to_save, "config_theme")
_config_int(to_save, "config_random_books")
_config_int(to_save, "config_books_per_page")
_config_int(to_save, "config_authors_max")
config.config_default_role = constants.selected_roles(to_save)
@ -536,10 +560,10 @@ def update_view_configuration():
return view_configuration()
@admi.route("/ajax/loaddialogtexts/<element_id>")
@admi.route("/ajax/loaddialogtexts/<element_id>", methods=['POST'])
@login_required
def load_dialogtexts(element_id):
texts = {"header": "", "main": ""}
texts = {"header": "", "main": "", "valid": 1}
if element_id == "config_delete_kobo_token":
texts["main"] = _('Do you really want to delete the Kobo Token?')
elif element_id == "btndeletedomain":
@ -558,6 +582,10 @@ def load_dialogtexts(element_id):
texts["main"] = _('Are you sure you want to change the selected restrictions for the selected user(s)?')
elif element_id == "sidebar_view":
texts["main"] = _('Are you sure you want to change the selected visibility restrictions for the selected user(s)?')
elif element_id == "kobo_only_shelves_sync":
texts["main"] = _('Are you sure you want to change shelf sync behavior for the selected user(s)?')
elif element_id == "db_submit":
texts["main"] = _('Are you sure you want to change Calibre libray location?')
return json.dumps(texts)
@ -862,14 +890,6 @@ def list_restriction(res_type, user_id):
return response
@admi.route("/basicconfig/pathchooser/")
@unconfigured
def config_pathchooser():
if filepicker:
return pathchooser()
abort(403)
@admi.route("/ajax/pathchooser/")
@login_required
@admin_required
@ -958,16 +978,6 @@ def pathchooser():
return json.dumps(context)
@admi.route("/basicconfig", methods=["GET", "POST"])
@unconfigured
def basic_configuration():
logout_user()
if request.method == "POST":
log.debug("Basic Configuration send")
return _configuration_update_helper(configured=filepicker)
return _configuration_result(configured=filepicker)
def _config_int(to_save, x, func=int):
return config.set_from_dictionary(to_save, x, func)
@ -986,23 +996,24 @@ def _config_string(to_save, x):
def _configuration_gdrive_helper(to_save):
gdrive_error = None
gdrive_secrets = {}
if to_save.get("config_use_google_drive"):
gdrive_secrets = {}
if not os.path.isfile(gdriveutils.SETTINGS_YAML):
config.config_use_google_drive = False
if not os.path.isfile(gdriveutils.SETTINGS_YAML):
config.config_use_google_drive = False
if gdrive_support:
gdrive_error = gdriveutils.get_error_text(gdrive_secrets)
if "config_use_google_drive" in to_save and not config.config_use_google_drive and not gdrive_error:
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
gdrive_secrets = json.load(settings)['web']
if not gdrive_secrets:
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]
)
if gdrive_support:
gdrive_error = gdriveutils.get_error_text(gdrive_secrets)
if "config_use_google_drive" in to_save and not config.config_use_google_drive and not gdrive_error:
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
gdrive_secrets = json.load(settings)['web']
if not gdrive_secrets:
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]
)
# 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)
@ -1036,23 +1047,23 @@ def _configuration_oauth_helper(to_save):
return reboot_required
def _configuration_logfile_helper(to_save, gdrive_error):
def _configuration_logfile_helper(to_save):
reboot_required = False
reboot_required |= _config_int(to_save, "config_log_level")
reboot_required |= _config_string(to_save, "config_logfile")
if not logger.is_valid_logfile(config.config_logfile):
return reboot_required, \
_configuration_result(_('Logfile Location is not Valid, Please Enter Correct Path'), gdrive_error)
_configuration_result(_('Logfile Location is not Valid, Please Enter Correct Path'))
reboot_required |= _config_checkbox_int(to_save, "config_access_log")
reboot_required |= _config_string(to_save, "config_access_logfile")
if not logger.is_valid_logfile(config.config_access_logfile):
return reboot_required, \
_configuration_result(_('Access Logfile Location is not Valid, Please Enter Correct Path'), gdrive_error)
_configuration_result(_('Access Logfile Location is not Valid, Please Enter Correct Path'))
return reboot_required, None
def _configuration_ldap_helper(to_save, gdrive_error):
def _configuration_ldap_helper(to_save):
reboot_required = False
reboot_required |= _config_string(to_save, "config_ldap_provider_url")
reboot_required |= _config_int(to_save, "config_ldap_port")
@ -1079,44 +1090,37 @@ def _configuration_ldap_helper(to_save, gdrive_error):
or not config.config_ldap_dn \
or not config.config_ldap_user_object:
return reboot_required, _configuration_result(_('Please Enter a LDAP Provider, '
'Port, DN and User Object Identifier'), gdrive_error)
'Port, DN and User Object Identifier'))
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password):
return reboot_required, _configuration_result('Please Enter a LDAP Service Account and Password',
gdrive_error)
return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account and Password'))
else:
if not config.config_ldap_serv_username:
return reboot_required, _configuration_result('Please Enter a LDAP Service Account', gdrive_error)
return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account'))
if config.config_ldap_group_object_filter:
if config.config_ldap_group_object_filter.count("%s") != 1:
return reboot_required, \
_configuration_result(_('LDAP Group Object Filter Needs to Have One "%s" Format Identifier'),
gdrive_error)
_configuration_result(_('LDAP Group Object Filter Needs to Have One "%s" Format Identifier'))
if config.config_ldap_group_object_filter.count("(") != config.config_ldap_group_object_filter.count(")"):
return reboot_required, _configuration_result(_('LDAP Group Object Filter Has Unmatched Parenthesis'),
gdrive_error)
return reboot_required, _configuration_result(_('LDAP Group Object Filter Has Unmatched Parenthesis'))
if config.config_ldap_user_object.count("%s") != 1:
return reboot_required, \
_configuration_result(_('LDAP User Object Filter needs to Have One "%s" Format Identifier'),
gdrive_error)
_configuration_result(_('LDAP User Object Filter needs to Have One "%s" Format Identifier'))
if config.config_ldap_user_object.count("(") != config.config_ldap_user_object.count(")"):
return reboot_required, _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'),
gdrive_error)
return reboot_required, _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'))
if to_save.get("ldap_import_user_filter") == '0':
config.config_ldap_member_user_object = ""
else:
if config.config_ldap_member_user_object.count("%s") != 1:
return reboot_required, \
_configuration_result(_('LDAP Member User Filter needs to Have One "%s" Format Identifier'),
gdrive_error)
_configuration_result(_('LDAP Member User Filter needs to Have One "%s" Format Identifier'))
if config.config_ldap_member_user_object.count("(") != config.config_ldap_member_user_object.count(")"):
return reboot_required, _configuration_result(_('LDAP Member User Filter Has Unmatched Parenthesis'),
gdrive_error)
return reboot_required, _configuration_result(_('LDAP Member User Filter Has Unmatched Parenthesis'))
if config.config_ldap_cacert_path or config.config_ldap_cert_path or config.config_ldap_key_path:
if not (os.path.isfile(config.config_ldap_cacert_path) and
@ -1124,13 +1128,31 @@ def _configuration_ldap_helper(to_save, gdrive_error):
os.path.isfile(config.config_ldap_key_path)):
return reboot_required, \
_configuration_result(_('LDAP CACertificate, Certificate or Key Location is not Valid, '
'Please Enter Correct Path'),
gdrive_error)
'Please Enter Correct Path'))
return reboot_required, None
def _configuration_update_helper(configured):
reboot_required = False
@admi.route("/ajax/simulatedbchange", methods=['POST'])
@login_required
@admin_required
def simulatedbchange():
db_change, db_valid = _db_simulate_change()
return Response(json.dumps({"change": db_change, "valid": db_valid}), mimetype='application/json')
def _db_simulate_change():
param = request.form.to_dict()
to_save = {}
to_save['config_calibre_dir'] = re.sub(r'[\\/]metadata\.db$',
'',
param['config_calibre_dir'],
flags=re.IGNORECASE).strip()
db_change = config.config_calibre_dir != to_save["config_calibre_dir"] and config.config_calibre_dir
db_valid = calibre_db.check_valid_db(to_save["config_calibre_dir"], ub.app_DB_path)
return db_change, db_valid
def _db_configuration_update_helper():
db_change = False
to_save = request.form.to_dict()
gdrive_error = None
@ -1140,24 +1162,47 @@ def _configuration_update_helper(configured):
to_save['config_calibre_dir'],
flags=re.IGNORECASE)
try:
db_change |= _config_string(to_save, "config_calibre_dir")
db_change, db_valid = _db_simulate_change()
# gdrive_error drive setup
gdrive_error = _configuration_gdrive_helper(to_save)
except (OperationalError, InvalidRequestError):
ub.session.rollback()
log.error("Settings DB is not Writeable")
_db_configuration_result(_("Settings DB is not Writeable"), 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):
gdriveutils.downloadFile(None, "metadata.db", metadata_db)
db_change = True
except Exception as ex:
return _db_configuration_result('{}'.format(ex), gdrive_error)
if db_change or not db_valid or not config.db_configured:
if not calibre_db.setup_db(to_save['config_calibre_dir'], ub.app_DB_path):
return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'),
gdrive_error)
_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")
# warning = {'type': "warning", 'message': _(u"DB is not Writeable")}
config.save()
return _db_configuration_result(None, gdrive_error)
def _configuration_update_helper():
reboot_required = False
to_save = request.form.to_dict()
try:
reboot_required |= _config_int(to_save, "config_port")
reboot_required |= _config_string(to_save, "config_keyfile")
if config.config_keyfile and not os.path.isfile(config.config_keyfile):
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'),
gdrive_error,
configured)
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'))
reboot_required |= _config_string(to_save, "config_certfile")
if config.config_certfile and not os.path.isfile(config.config_certfile):
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'),
gdrive_error,
configured)
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'))
_config_checkbox_int(to_save, "config_uploading")
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
@ -1181,15 +1226,14 @@ def _configuration_update_helper(configured):
reboot_required |= _config_int(to_save, "config_login_type")
# LDAP configurator,
# LDAP configurator
if config.config_login_type == constants.LOGIN_LDAP:
reboot, message = _configuration_ldap_helper(to_save, gdrive_error)
reboot, message = _configuration_ldap_helper(to_save)
if message:
return message
reboot_required |= reboot
# Remote login configuration
_config_checkbox(to_save, "config_remote_login")
if not config.config_remote_login:
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.token_type == 0).delete()
@ -1213,7 +1257,7 @@ def _configuration_update_helper(configured):
if config.config_login_type == constants.LOGIN_OAUTH:
reboot_required |= _configuration_oauth_helper(to_save)
reboot, message = _configuration_logfile_helper(to_save, gdrive_error)
reboot, message = _configuration_logfile_helper(to_save)
if message:
return message
reboot_required |= reboot
@ -1222,67 +1266,55 @@ def _configuration_update_helper(configured):
if "config_rarfile_location" in to_save:
unrar_status = helper.check_unrar(config.config_rarfile_location)
if unrar_status:
return _configuration_result(unrar_status, gdrive_error, configured)
return _configuration_result(unrar_status)
except (OperationalError, InvalidRequestError):
ub.session.rollback()
log.error("Settings DB is not Writeable")
_configuration_result(_("Settings DB is not Writeable"), gdrive_error, configured)
try:
metadata_db = os.path.join(config.config_calibre_dir, "metadata.db")
if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db):
gdriveutils.downloadFile(None, "metadata.db", metadata_db)
db_change = True
except Exception as ex:
return _configuration_result('%s' % ex, gdrive_error, configured)
if db_change:
if not calibre_db.setup_db(config, ub.app_DB_path):
return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'),
gdrive_error,
configured)
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
flash(_(u"DB is not Writeable"), category="warning")
_configuration_result(_("Settings DB is not Writeable"))
config.save()
flash(_(u"Calibre-Web configuration updated"), category="success")
if reboot_required:
web_server.stop(True)
return _configuration_result(None, gdrive_error, configured)
return _configuration_result(None, reboot_required)
def _configuration_result(error_flash=None, reboot=False):
resp = {}
if error_flash:
log.error(error_flash)
config.load()
resp['result'] = [{'type': "danger", 'message': error_flash}]
else:
resp['result'] = [{'type': "success", 'message':_(u"Calibre-Web configuration updated")}]
resp['reboot'] = reboot
resp['config_upload']= config.config_upload_formats
return Response(json.dumps(resp), mimetype='application/json')
def _configuration_result(error_flash=None, gdrive_error=None, configured=True):
def _db_configuration_result(error_flash=None, gdrive_error=None):
gdrive_authenticate = not is_gdrive_ready()
gdrivefolders = []
if gdrive_error is None:
if not gdrive_error and config.config_use_google_drive:
gdrive_error = gdriveutils.get_error_text()
if gdrive_error and gdrive_support:
log.error(gdrive_error)
gdrive_error = _(gdrive_error)
flash(gdrive_error, category="error")
else:
if not gdrive_authenticate and gdrive_support:
gdrivefolders = gdriveutils.listRootFolders()
show_back_button = current_user.is_authenticated
show_login_button = config.db_configured and not current_user.is_authenticated
if error_flash:
log.error(error_flash)
config.load()
flash(error_flash, category="error")
show_login_button = False
return render_title_template("config_edit.html",
return render_title_template("config_db.html",
config=config,
provider=oauthblueprints,
show_back_button=show_back_button,
show_login_button=show_login_button,
show_authenticate_google_drive=gdrive_authenticate,
filepicker=configured,
gdriveError=gdrive_error,
gdrivefolders=gdrivefolders,
feature_support=feature_support,
title=_(u"Basic Configuration"), page="config")
title=_(u"Database Configuration"), page="dbconfig")
def _handle_new_user(to_save, content, languages, translations, kobo_support):
@ -1317,6 +1349,7 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
content.denied_tags = config.config_denied_tags
content.allowed_column_value = config.config_allowed_column_value
content.denied_column_value = config.config_denied_column_value
content.kobo_only_shelves_sync = 0 # No default value for kobo sync shelf setting
ub.session.add(content)
ub.session.commit()
flash(_(u"User '%(user)s' created", user=content.name), category="success")
@ -1384,6 +1417,8 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
else:
content.sidebar_view &= ~constants.DETAIL_RANDOM
content.kobo_only_shelves_sync = int(to_save.get("kobo_only_shelves_sync") == "on") or 0
if to_save.get("default_language"):
content.default_language = to_save["default_language"]
if to_save.get("locale"):

View File

@ -45,7 +45,6 @@ parser.add_argument('-v', '--version', action='version', help='Shows version num
version=version_info())
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password')
parser.add_argument('-f', action='store_true', help='Enables filepicker in unconfigured mode')
args = parser.parse_args()
if sys.version_info < (3, 0):
@ -114,6 +113,3 @@ user_credentials = args.s or None
if user_credentials and ":" not in user_credentials:
print("No valid 'username:password' format")
sys.exit(3)
# Handles enabling of filepicker
filepicker = args.f or None

View File

@ -347,7 +347,7 @@ class _ConfigSQL(object):
log.error(error)
log.warning("invalidating configuration")
self.db_configured = False
self.config_calibre_dir = None
# self.config_calibre_dir = None
self.save()

View File

@ -154,7 +154,7 @@ def selected_roles(dictionary):
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
'series_id, languages, publisher')
STABLE_VERSION = {'version': '0.6.12 Beta'}
STABLE_VERSION = {'version': '0.6.13 Beta'}
NIGHTLY_VERSION = {}
NIGHTLY_VERSION[0] = '$Format:%H$'

View File

@ -524,19 +524,44 @@ class CalibreDB():
return cc_classes
@classmethod
def setup_db(cls, config, app_db_path):
def check_valid_db(cls, config_calibre_dir, app_db_path):
if not config_calibre_dir:
return False
dbpath = os.path.join(config_calibre_dir, "metadata.db")
if not os.path.exists(dbpath):
return False
try:
check_engine = create_engine('sqlite://',
echo=False,
isolation_level="SERIALIZABLE",
connect_args={'check_same_thread': False},
poolclass=StaticPool)
with check_engine.begin() as connection:
connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
check_engine.connect()
except Exception:
return False
return True
@classmethod
def update_config(cls, config):
cls.config = config
@classmethod
def setup_db(cls, config_calibre_dir, app_db_path):
# cls.config = config
cls.dispose()
# toDo: if db changed -> delete shelfs, delete download books, delete read boks, kobo sync??
if not config.config_calibre_dir:
config.invalidate()
if not config_calibre_dir:
cls.config.invalidate()
return False
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
dbpath = os.path.join(config_calibre_dir, "metadata.db")
if not os.path.exists(dbpath):
config.invalidate()
cls.config.invalidate()
return False
try:
@ -552,14 +577,14 @@ class CalibreDB():
conn = cls.engine.connect()
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
except Exception as ex:
config.invalidate(ex)
cls.config.invalidate(ex)
return False
config.db_configured = True
cls.config.db_configured = True
if not cc_classes:
try:
cc = conn.execute("SELECT id, datatype FROM custom_columns")
cc = conn.execute(text("SELECT id, datatype FROM custom_columns"))
cls.setup_db_cc_classes(cc)
except OperationalError as e:
log.debug_or_exception(e)
@ -828,7 +853,8 @@ class CalibreDB():
def reconnect_db(self, config, app_db_path):
self.dispose()
self.engine.dispose()
self.setup_db(config, app_db_path)
self.setup_db(config.config_calibre_dir, app_db_path)
self.update_config(config)
def lcase(s):

View File

@ -29,7 +29,7 @@ except ImportError:
import os
from flask import send_file
from flask import send_file, __version__
from . import logger, config
from .about import collect_stats
@ -43,9 +43,15 @@ def assemble_logfiles(file_name):
with open(f, 'r') as fd:
shutil.copyfileobj(fd, wfd)
wfd.seek(0)
return send_file(wfd,
as_attachment=True,
attachment_filename=os.path.basename(file_name))
if int(__version__.split('.')[0]) < 2:
return send_file(wfd,
as_attachment=True,
attachment_filename=os.path.basename(file_name))
else:
return send_file(wfd,
as_attachment=True,
download_name=os.path.basename(file_name))
def send_debug():
file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*')
@ -60,6 +66,11 @@ def send_debug():
for fp in file_list:
zf.write(fp, os.path.basename(fp))
memory_zip.seek(0)
return send_file(memory_zip,
as_attachment=True,
attachment_filename="Calibre-Web-debug-pack.zip")
if int(__version__.split('.')[0]) < 2:
return send_file(memory_zip,
as_attachment=True,
attachment_filename="Calibre-Web-debug-pack.zip")
else:
return send_file(memory_zip,
as_attachment=True,
download_name="Calibre-Web-debug-pack.zip")

View File

@ -1148,11 +1148,15 @@ def edit_list_book(param):
'newValue': ' & '.join([author.replace('|',',') for author in input_authors])}),
mimetype='application/json')
book.last_modified = datetime.utcnow()
calibre_db.session.commit()
# revert change for sort if automatic fields link is deactivated
if param == 'title' and vals.get('checkT') == "false":
book.sort = sort
try:
calibre_db.session.commit()
# revert change for sort if automatic fields link is deactivated
if param == 'title' and vals.get('checkT') == "false":
book.sort = sort
calibre_db.session.commit()
except (OperationalError, IntegrityError) as e:
calibre_db.session.rollback()
log.error("Database error: %s", e)
return ret
@ -1224,3 +1228,43 @@ def merge_list_book():
delete_book(from_book.id,"", True)
return json.dumps({'success': True})
return ""
@editbook.route("/ajax/xchange", methods=['POST'])
@login_required
@edit_required
def table_xchange_author_title():
vals = request.get_json().get('xchange')
if vals:
for val in vals:
modif_date = False
book = calibre_db.get_book(val)
authors = book.title
entries = calibre_db.order_authors(book)
author_names = []
for authr in entries.authors:
author_names.append(authr.name.replace('|', ','))
title_change = handle_title_on_edit(book, " ".join(author_names))
input_authors, authorchange = handle_author_on_edit(book, authors)
if authorchange or title_change:
edited_books_id = book.id
modif_date = True
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
if edited_books_id:
helper.update_dir_stucture(edited_books_id, config.config_calibre_dir, input_authors[0])
if modif_date:
book.last_modified = datetime.utcnow()
try:
calibre_db.session.commit()
except (OperationalError, IntegrityError) as e:
calibre_db.session.rollback()
log.error("Database error: %s", e)
return json.dumps({'success': False})
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
return json.dumps({'success': True})
return ""

View File

@ -35,6 +35,7 @@ def error_http(error):
error_code="Error {0}".format(error.code),
error_name=error.name,
issue=False,
unconfigured=not config.db_configured,
instance=config.config_calibre_web_title
), error.code
@ -44,6 +45,7 @@ def internal_error(error):
error_code="Internal Server Error",
error_name=str(error),
issue=True,
unconfigured=False,
error_stack=traceback.format_exc().split("\n"),
instance=config.config_calibre_web_title
), 500

View File

@ -74,7 +74,7 @@ def google_drive_callback():
f.write(credentials.to_json())
except (ValueError, AttributeError) as error:
log.error(error)
return redirect(url_for('admin.configuration'))
return redirect(url_for('admin.db_configuration'))
@gdrive.route("/watch/subscribe")
@ -99,7 +99,7 @@ def watch_gdrive():
else:
flash(reason['message'], category="error")
return redirect(url_for('admin.configuration'))
return redirect(url_for('admin.db_configuration'))
@gdrive.route("/watch/revoke")
@ -115,7 +115,7 @@ def revoke_watch_gdrive():
pass
config.config_google_drive_watch_changes_response = {}
config.save()
return redirect(url_for('admin.configuration'))
return redirect(url_for('admin.db_configuration'))
@gdrive.route("/watch/callback", methods=['GET', 'POST'])

View File

@ -34,6 +34,7 @@ try:
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.exc import OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import text
try:
from apiclient import errors
@ -168,7 +169,7 @@ class PermissionAdded(Base):
def migrate():
if not engine.dialect.has_table(engine.connect(), "permissions_added"):
PermissionAdded.__table__.create(bind = engine)
for sql in session.execute("select sql from sqlite_master where type='table'"):
for sql in session.execute(text("select sql from sqlite_master where type='table'")):
if 'CREATE TABLE gdrive_ids' in sql[0]:
currUniqueConstraint = 'UNIQUE (gdrive_id)'
if currUniqueConstraint in sql[0]:

View File

@ -42,7 +42,7 @@ from flask import (
from flask_login import current_user
from werkzeug.datastructures import Headers
from sqlalchemy import func
from sqlalchemy.sql.expression import and_
from sqlalchemy.sql.expression import and_, or_
from sqlalchemy.exc import StatementError
import requests
@ -81,6 +81,7 @@ CONNECTION_SPECIFIC_HEADERS = [
"transfer-encoding",
]
def get_kobo_activated():
return config.config_kobo_sync
@ -151,32 +152,42 @@ def HandleSyncRequest():
# in case of external changes (e.g: adding a book through Calibre).
calibre_db.reconnect_db(config, ub.app_DB_path)
if sync_token.books_last_id > -1:
only_kobo_shelves = current_user.kobo_only_shelves_sync
# calibre_db.session.query(ub.Shelf).filter(ub.Shelf.user_id == current_user.id).filter(ub.Shelf.kobo_sync).count() > 0
if only_kobo_shelves:
changed_entries = (
calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
.filter(db.Books.last_modified >= sync_token.books_last_modified)
.filter(db.Books.id>sync_token.books_last_id)
.filter(db.Data.format.in_(KOBO_FORMATS))
.filter(calibre_db.common_filters())
.order_by(db.Books.last_modified)
.order_by(db.Books.id)
.limit(SYNC_ITEM_LIMIT)
calibre_db.session.query(db.Books,
ub.ArchivedBook.last_modified,
ub.BookShelf.date_added,
ub.ArchivedBook.is_archived)
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
.filter(or_(db.Books.last_modified > sync_token.books_last_modified,
ub.BookShelf.date_added > sync_token.books_last_modified))
.filter(db.Data.format.in_(KOBO_FORMATS)).filter(calibre_db.common_filters())
.order_by(db.Books.id)
.order_by(ub.ArchivedBook.last_modified)
.join(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)
.join(ub.Shelf)
.filter(ub.Shelf.kobo_sync)
.distinct()
)
else:
changed_entries = (
calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
.filter(db.Books.last_modified > sync_token.books_last_modified)
.filter(db.Data.format.in_(KOBO_FORMATS))
.filter(calibre_db.common_filters())
.order_by(db.Books.last_modified)
.order_by(db.Books.id)
.limit(SYNC_ITEM_LIMIT)
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
.filter(db.Books.last_modified > sync_token.books_last_modified)
.filter(calibre_db.common_filters())
.filter(db.Data.format.in_(KOBO_FORMATS))
.order_by(db.Books.last_modified)
.order_by(db.Books.id)
)
if sync_token.books_last_id > -1:
changed_entries = changed_entries.filter(db.Books.id > sync_token.books_last_id)
reading_states_in_new_entitlements = []
for book in changed_entries:
for book in changed_entries.limit(SYNC_ITEM_LIMIT):
formats = [data.format for data in book.Books.data]
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
@ -192,7 +203,14 @@ def HandleSyncRequest():
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
reading_states_in_new_entitlements.append(book.Books.id)
if book.Books.timestamp > sync_token.books_last_created:
ts_created = book.Books.timestamp
try:
ts_created = max(ts_created, book.date_added)
except AttributeError:
pass
if ts_created > sync_token.books_last_created:
sync_results.append({"NewEntitlement": entitlement})
else:
sync_results.append({"ChangedEntitlement": entitlement})
@ -200,35 +218,48 @@ def HandleSyncRequest():
new_books_last_modified = max(
book.Books.last_modified, new_books_last_modified
)
new_books_last_created = max(book.Books.timestamp, new_books_last_created)
try:
new_books_last_modified = max(
new_books_last_modified, book.date_added
)
except AttributeError:
pass
new_books_last_created = max(ts_created, new_books_last_created)
max_change = changed_entries.from_self().filter(ub.ArchivedBook.is_archived)\
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
max_change = max_change.last_modified if max_change else new_archived_last_modified
max_change = (changed_entries
.from_self()
.filter(ub.ArchivedBook.is_archived)
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc())
.first()
)
if max_change:
max_change = max_change.last_modified
else:
max_change = new_archived_last_modified
new_archived_last_modified = max(new_archived_last_modified, max_change)
# no. of books returned
book_count = changed_entries.count()
# last entry:
if book_count:
books_last_id = changed_entries.all()[-1].Books.id or -1
else:
books_last_id = -1
books_last_id = changed_entries.all()[-1].Books.id or -1 if book_count else -1
# generate reading state data
changed_reading_states = (
ub.session.query(ub.KoboReadingState)
.filter(and_(func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified,
ub.KoboReadingState.user_id == current_user.id,
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements))))
changed_reading_states = ub.session.query(ub.KoboReadingState)
if only_kobo_shelves:
changed_reading_states = changed_reading_states.join(ub.BookShelf,
ub.KoboReadingState.book_id == ub.BookShelf.book_id)\
.join(ub.Shelf)\
.filter(ub.Shelf.kobo_sync,
or_(
func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified,
ub.BookShelf.date_added > sync_token.books_last_modified
)).distinct()
else:
changed_reading_states = changed_reading_states.filter(
func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified)
changed_reading_states = changed_reading_states.filter(
and_(ub.KoboReadingState.user_id == current_user.id,
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements)))
for kobo_reading_state in changed_reading_states.all():
book = calibre_db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one_or_none()
if book:
@ -239,7 +270,7 @@ def HandleSyncRequest():
})
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
sync_shelves(sync_token, sync_results)
sync_shelves(sync_token, sync_results, only_kobo_shelves)
sync_token.books_last_created = new_books_last_created
sync_token.books_last_modified = new_books_last_modified
@ -394,7 +425,7 @@ def get_metadata(book):
book_uuid = book.uuid
metadata = {
"Categories": ["00000000-0000-0000-0000-000000000001",],
"Categories": ["00000000-0000-0000-0000-000000000001", ],
# "Contributors": get_author(book),
"CoverImageId": book_uuid,
"CrossRevisionId": book_uuid,
@ -601,13 +632,14 @@ def HandleTagRemoveItem(tag_id):
# Add new, changed, or deleted shelves to the sync_results.
# Note: Public shelves that aren't owned by the user aren't supported.
def sync_shelves(sync_token, sync_results):
def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
new_tags_last_modified = sync_token.tags_last_modified
for shelf in ub.session.query(ub.ShelfArchive).filter(func.datetime(ub.ShelfArchive.last_modified) > sync_token.tags_last_modified,
ub.ShelfArchive.user_id == current_user.id):
for shelf in ub.session.query(ub.ShelfArchive).filter(
func.datetime(ub.ShelfArchive.last_modified) > sync_token.tags_last_modified,
ub.ShelfArchive.user_id == current_user.id
):
new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified)
sync_results.append({
"DeletedTag": {
"Tag": {
@ -617,8 +649,29 @@ def sync_shelves(sync_token, sync_results):
}
})
for shelf in ub.session.query(ub.Shelf).filter(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
ub.Shelf.user_id == current_user.id):
extra_filters = []
if only_kobo_shelves:
for shelf in ub.session.query(ub.Shelf).filter(
func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
ub.Shelf.user_id == current_user.id,
not ub.Shelf.kobo_sync
):
sync_results.append({
"DeletedTag": {
"Tag": {
"Id": shelf.uuid,
"LastModified": convert_to_kobo_timestamp_string(shelf.last_modified)
}
}
})
extra_filters.append(ub.Shelf.kobo_sync)
for shelf in ub.session.query(ub.Shelf).outerjoin(ub.BookShelf).filter(
or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
ub.BookShelf.date_added > sync_token.tags_last_modified),
ub.Shelf.user_id == current_user.id,
*extra_filters
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc()):
if not shelf_lib.check_shelf_view_permissions(shelf):
continue

View File

@ -21,20 +21,20 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from datetime import datetime
import sys
from datetime import datetime
from flask import Blueprint, request, flash, redirect, url_for
from flask import Blueprint, flash, redirect, request, url_for
from flask_babel import gettext as _
from flask_login import login_required, current_user
from flask_login import current_user, login_required
from sqlalchemy.exc import InvalidRequestError, OperationalError
from sqlalchemy.sql.expression import func, true
from sqlalchemy.exc import OperationalError, InvalidRequestError
from . import logger, ub, calibre_db, db
from . import calibre_db, config, db, logger, ub
from .render_template import render_title_template
from .usermanagement import login_required_if_no_ano
shelf = Blueprint('shelf', __name__)
log = logger.create()
@ -240,15 +240,16 @@ def edit_shelf(shelf_id):
# if shelf ID is set, we are editing a shelf
def create_edit_shelf(shelf, title, page, shelf_id=False):
sync_only_selected_shelves = current_user.kobo_only_shelves_sync
# calibre_db.session.query(ub.Shelf).filter(ub.Shelf.user_id == current_user.id).filter(ub.Shelf.kobo_sync).count()
if request.method == "POST":
to_save = request.form.to_dict()
if "is_public" in to_save:
shelf.is_public = 1
else:
shelf.is_public = 0
shelf.is_public = 1 if to_save.get("is_public") else 0
if config.config_kobo_sync:
shelf.kobo_sync = True if to_save.get("kobo_sync") else False
if check_shelf_is_unique(shelf, to_save, shelf_id):
shelf.name = to_save["title"]
# shelf.last_modified = datetime.utcnow()
if not shelf_id:
shelf.user_id = int(current_user.id)
ub.session.add(shelf)
@ -271,7 +272,12 @@ def create_edit_shelf(shelf, title, page, shelf_id=False):
ub.session.rollback()
log.debug_or_exception(ex)
flash(_(u"There was an error"), category="error")
return render_title_template('shelf_edit.html', shelf=shelf, title=title, page=page)
return render_title_template('shelf_edit.html',
shelf=shelf,
title=title,
page=page,
kobo_sync_enabled=config.config_kobo_sync,
sync_only_selected_shelves=sync_only_selected_shelves)
def check_shelf_is_unique(shelf, to_save, shelf_id=False):
@ -362,8 +368,8 @@ def order_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
result = list()
if shelf and check_shelf_view_permissions(shelf):
result = calibre_db.session.query(db.Books)\
.join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id , isouter=True) \
result = calibre_db.session.query(db.Books) \
.join(ub.BookShelf, ub.BookShelf.book_id == db.Books.id, isouter=True) \
.add_columns(calibre_db.common_filters().label("visible")) \
.filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all()
return render_title_template('shelf_order.html', entries=result,
@ -372,7 +378,7 @@ def order_shelf(shelf_id):
def change_shelf_order(shelf_id, order):
result = calibre_db.session.query(db.Books).join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id)\
result = calibre_db.session.query(db.Books).join(ub.BookShelf, ub.BookShelf.book_id == db.Books.id) \
.filter(ub.BookShelf.shelf == shelf_id).order_by(*order).all()
for index, entry in enumerate(result):
book = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
@ -412,13 +418,13 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
page = 'shelfdown.html'
result, __, pagination = calibre_db.fill_indexpage(page_no, pagesize,
db.Books,
ub.BookShelf.shelf == shelf_id,
[ub.BookShelf.order.asc()],
ub.BookShelf,ub.BookShelf.book_id == db.Books.id)
db.Books,
ub.BookShelf.shelf == shelf_id,
[ub.BookShelf.order.asc()],
ub.BookShelf, ub.BookShelf.book_id == db.Books.id)
# delete chelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
wrong_entries = calibre_db.session.query(ub.BookShelf)\
.join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True)\
wrong_entries = calibre_db.session.query(ub.BookShelf) \
.join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True) \
.filter(db.Books.id == None).all()
for entry in wrong_entries:
log.info('Not existing book {} in {} deleted'.format(entry.book_id, shelf))

View File

@ -445,7 +445,7 @@
--overlay-button-bg-color: rgba(12, 12, 13, 0.1);
--overlay-button-hover-color: rgba(12, 12, 13, 0.3);
--loading-icon: url(images/loading.svg);
/*--loading-icon: url(images/loading.svg);
--treeitem-expanded-icon: url(images/treeitem-expanded.svg);
--treeitem-collapsed-icon: url(images/treeitem-collapsed.svg);
--toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow.svg);
@ -479,7 +479,7 @@
--secondaryToolbarButton-spreadNone-icon: url(images/secondaryToolbarButton-spreadNone.svg);
--secondaryToolbarButton-spreadOdd-icon: url(images/secondaryToolbarButton-spreadOdd.svg);
--secondaryToolbarButton-spreadEven-icon: url(images/secondaryToolbarButton-spreadEven.svg);
--secondaryToolbarButton-documentProperties-icon: url(images/secondaryToolbarButton-documentProperties.svg);
--secondaryToolbarButton-documentProperties-icon: url(images/secondaryToolbarButton-documentProperties.svg);*/
}
@media (prefers-color-scheme: dark) {
@ -515,42 +515,6 @@
--doorhanger-separator-color: rgba(92, 92, 97, 1);
--overlay-button-bg-color: rgba(92, 92, 97, 1);
--overlay-button-hover-color: rgba(115, 115, 115, 1);
--loading-icon: url(images/loading-dark.svg);
--treeitem-expanded-icon: url(images/treeitem-expanded-dark.svg);
--treeitem-collapsed-icon: url(images/treeitem-collapsed-dark.svg);
--toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow-dark.svg);
--toolbarButton-sidebarToggle-icon: url(images/toolbarButton-sidebarToggle-dark.svg);
--toolbarButton-secondaryToolbarToggle-icon: url(images/toolbarButton-secondaryToolbarToggle-dark.svg);
--toolbarButton-pageUp-icon: url(images/toolbarButton-pageUp-dark.svg);
--toolbarButton-pageDown-icon: url(images/toolbarButton-pageDown-dark.svg);
--toolbarButton-zoomOut-icon: url(images/toolbarButton-zoomOut-dark.svg);
--toolbarButton-zoomIn-icon: url(images/toolbarButton-zoomIn-dark.svg);
--toolbarButton-presentationMode-icon: url(images/toolbarButton-presentationMode-dark.svg);
--toolbarButton-print-icon: url(images/toolbarButton-print-dark.svg);
--toolbarButton-openFile-icon: url(images/toolbarButton-openFile-dark.svg);
--toolbarButton-download-icon: url(images/toolbarButton-download-dark.svg);
--toolbarButton-bookmark-icon: url(images/toolbarButton-bookmark-dark.svg);
--toolbarButton-viewThumbnail-icon: url(images/toolbarButton-viewThumbnail-dark.svg);
--toolbarButton-viewOutline-icon: url(images/toolbarButton-viewOutline-dark.svg);
--toolbarButton-viewAttachments-icon: url(images/toolbarButton-viewAttachments-dark.svg);
--toolbarButton-viewLayers-icon: url(images/toolbarButton-viewLayers-dark.svg);
--toolbarButton-search-icon: url(images/toolbarButton-search-dark.svg);
--findbarButton-previous-icon: url(images/findbarButton-previous-dark.svg);
--findbarButton-next-icon: url(images/findbarButton-next-dark.svg);
--secondaryToolbarButton-firstPage-icon: url(images/secondaryToolbarButton-firstPage-dark.svg);
--secondaryToolbarButton-lastPage-icon: url(images/secondaryToolbarButton-lastPage-dark.svg);
--secondaryToolbarButton-rotateCcw-icon: url(images/secondaryToolbarButton-rotateCcw-dark.svg);
--secondaryToolbarButton-rotateCw-icon: url(images/secondaryToolbarButton-rotateCw-dark.svg);
--secondaryToolbarButton-selectTool-icon: url(images/secondaryToolbarButton-selectTool-dark.svg);
--secondaryToolbarButton-handTool-icon: url(images/secondaryToolbarButton-handTool-dark.svg);
--secondaryToolbarButton-scrollVertical-icon: url(images/secondaryToolbarButton-scrollVertical-dark.svg);
--secondaryToolbarButton-scrollHorizontal-icon: url(images/secondaryToolbarButton-scrollHorizontal-dark.svg);
--secondaryToolbarButton-scrollWrapped-icon: url(images/secondaryToolbarButton-scrollWrapped-dark.svg);
--secondaryToolbarButton-spreadNone-icon: url(images/secondaryToolbarButton-spreadNone-dark.svg);
--secondaryToolbarButton-spreadOdd-icon: url(images/secondaryToolbarButton-spreadOdd-dark.svg);
--secondaryToolbarButton-spreadEven-icon: url(images/secondaryToolbarButton-spreadEven-dark.svg);
--secondaryToolbarButton-documentProperties-icon: url(images/secondaryToolbarButton-documentProperties-dark.svg);
}
}
@ -1686,7 +1650,7 @@ html[dir="rtl"] #findInput {
}
#findInput[data-status="pending"] {
background-image: url(images/loading.svg);
background-image: var(--loading-icon);
/*background-image: var(--loading-icon);*/
background-repeat: no-repeat;
background-position: 98%;
}
@ -1694,7 +1658,7 @@ html[dir="rtl"] #findInput {
#findInput[data-status="pending"] {
background-image: url(images/loading-dark.svg);
background-image: var(--loading-icon);
/*background-image: var(--loading-icon);*/
}
}
html[dir="rtl"] #findInput[data-status="pending"] {
@ -2342,7 +2306,7 @@ html[dir="rtl"] #toolbarViewerLeft > .toolbarButton:first-child {
display: inline-block;
top: 6px;
content: url(images/toolbarButton-menuArrow.svg);
content: var(--toolbarButton-menuArrow-icon);
/*content: var(--toolbarButton-menuArrow-icon);*/
pointer-events: none;
max-width: 16px;
}
@ -2350,7 +2314,7 @@ html[dir="rtl"] #toolbarViewerLeft > .toolbarButton:first-child {
.dropdownToolbarButton::after {
content: url(images/toolbarButton-menuArrow-dark.svg);
content: var(--toolbarButton-menuArrow-icon);
/*content: var(--toolbarButton-menuArrow-icon);*/
}
}
html[dir="ltr"] .dropdownToolbarButton::after {
@ -2491,14 +2455,14 @@ html[dir="rtl"] .secondaryToolbarButton::before {
.toolbarButton#sidebarToggle::before {
content: url(images/toolbarButton-sidebarToggle.svg);
content: var(--toolbarButton-sidebarToggle-icon);
/*content: var(--toolbarButton-sidebarToggle-icon);*/
}
@media (prefers-color-scheme: dark) {
.toolbarButton#sidebarToggle::before {
content: url(images/toolbarButton-sidebarToggle-dark.svg);
content: var(--toolbarButton-sidebarToggle-icon);
/*content: var(--toolbarButton-sidebarToggle-icon);*/
}
}
html[dir="rtl"] .toolbarButton#sidebarToggle::before {
@ -2507,14 +2471,14 @@ html[dir="rtl"] .toolbarButton#sidebarToggle::before {
.toolbarButton#secondaryToolbarToggle::before {
content: url(images/toolbarButton-secondaryToolbarToggle.svg);
content: var(--toolbarButton-secondaryToolbarToggle-icon);
/*content: var(--toolbarButton-secondaryToolbarToggle-icon);*/
}
@media (prefers-color-scheme: dark) {
.toolbarButton#secondaryToolbarToggle::before {
content: url(images/toolbarButton-secondaryToolbarToggle-dark.svg);
content: var(--toolbarButton-secondaryToolbarToggle-icon);
/*content: var(--toolbarButton-secondaryToolbarToggle-icon);*/
}
}
html[dir="rtl"] .toolbarButton#secondaryToolbarToggle::before {
@ -2523,14 +2487,14 @@ html[dir="rtl"] .toolbarButton#secondaryToolbarToggle::before {
.toolbarButton.findPrevious::before {
content: url(images/findbarButton-previous.svg);
content: var(--findbarButton-previous-icon);
/*content: var(--findbarButton-previous-icon);*/
}
@media (prefers-color-scheme: dark) {
.toolbarButton.findPrevious::before {
content: url(images/findbarButton-previous-dark.svg);
content: var(--findbarButton-previous-icon);
/*content: var(--findbarButton-previous-icon);*/
}
}
html[dir="rtl"] .toolbarButton.findPrevious::before {
@ -2539,14 +2503,14 @@ html[dir="rtl"] .toolbarButton.findPrevious::before {
.toolbarButton.findNext::before {
content: url(images/findbarButton-next.svg);
content: var(--findbarButton-next-icon);
/*content: var(--findbarButton-next-icon);*/
}
@media (prefers-color-scheme: dark) {
.toolbarButton.findNext::before {
content: url(images/findbarButton-next-dark.svg);
content: var(--findbarButton-next-icon);
/*content: var(--findbarButton-next-icon);*/
}
}
html[dir="rtl"] .toolbarButton.findNext::before {
@ -2555,14 +2519,14 @@ html[dir="rtl"] .toolbarButton.findNext::before {
.toolbarButton.pageUp::before {
content: url(images/toolbarButton-pageUp.svg);
content: var(--toolbarButton-pageUp-icon);
/*content: var(--toolbarButton-pageUp-icon);*/
}
@media (prefers-color-scheme: dark) {
.toolbarButton.pageUp::before {
content: url(images/toolbarButton-pageUp-dark.svg);
content: var(--toolbarButton-pageUp-icon);
/*content: var(--toolbarButton-pageUp-icon);*/
}
}
html[dir="rtl"] .toolbarButton.pageUp::before {
@ -2571,14 +2535,14 @@ html[dir="rtl"] .toolbarButton.pageUp::before {
.toolbarButton.pageDown::before {
content: url(images/toolbarButton-pageDown.svg);
content: var(--toolbarButton-pageDown-icon);
/*content: var(--toolbarButton-pageDown-icon);*/
}
@media (prefers-color-scheme: dark) {
.toolbarButton.pageDown::before {
content: url(images/toolbarButton-pageDown-dark.svg);
content: var(--toolbarButton-pageDown-icon);
/*content: var(--toolbarButton-pageDown-icon);*/
}
}
html[dir="rtl"] .toolbarButton.pageDown::before {
@ -2587,131 +2551,131 @@ html[dir="rtl"] .toolbarButton.pageDown::before {
.toolbarButton.zoomOut::before {
content: url(images/toolbarButton-zoomOut.svg);
content: var(--toolbarButton-zoomOut-icon);
/*content: var(--toolbarButton-zoomOut-icon);*/
}
@media (prefers-color-scheme: dark) {
.toolbarButton.zoomOut::before {
content: url(images/toolbarButton-zoomOut-dark.svg);
content: var(--toolbarButton-zoomOut-icon);
/*content: var(--toolbarButton-zoomOut-icon);*/
}
}
.toolbarButton.zoomIn::before {
content: url(images/toolbarButton-zoomIn.svg);
content: var(--toolbarButton-zoomIn-icon);
/*content: var(--toolbarButton-zoomIn-icon);*/
}
@media (prefers-color-scheme: dark) {
.toolbarButton.zoomIn::before {
content: url(images/toolbarButton-zoomIn-dark.svg);
content: var(--toolbarButton-zoomIn-icon);
/*content: var(--toolbarButton-zoomIn-icon);*/
}
}
.toolbarButton.presentationMode::before {
content: url(images/toolbarButton-presentationMode.svg);
content: var(--toolbarButton-presentationMode-icon);
/*content: var(--toolbarButton-presentationMode-icon);*/
}
@media (prefers-color-scheme: dark) {
.toolbarButton.presentationMode::before {
content: url(images/toolbarButton-presentationMode-dark.svg);
content: var(--toolbarButton-presentationMode-icon);
/*content: var(--toolbarButton-presentationMode-icon);*/
}
}
.secondaryToolbarButton.presentationMode::before {
content: url(images/toolbarButton-presentationMode.svg);
content: var(--toolbarButton-presentationMode-icon);
/*content: var(--toolbarButton-presentationMode-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.presentationMode::before {
content: url(images/toolbarButton-presentationMode-dark.svg);
content: var(--toolbarButton-presentationMode-icon);
/*content: var(--toolbarButton-presentationMode-icon);*/
}
}
.toolbarButton.print::before {
content: url(images/toolbarButton-print.svg);
content: var(--toolbarButton-print-icon);
/*content: var(--toolbarButton-print-icon);*/
}
@media (prefers-color-scheme: dark) {
.toolbarButton.print::before {
content: url(images/toolbarButton-print-dark.svg);
content: var(--toolbarButton-print-icon);
/*content: var(--toolbarButton-print-icon);*/
}
}
.secondaryToolbarButton.print::before {
content: url(images/toolbarButton-print.svg);
content: var(--toolbarButton-print-icon);
/*content: var(--toolbarButton-print-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.print::before {
content: url(images/toolbarButton-print-dark.svg);
content: var(--toolbarButton-print-icon);
/*content: var(--toolbarButton-print-icon);*/
}
}
.toolbarButton.openFile::before {
content: url(images/toolbarButton-openFile.svg);
content: var(--toolbarButton-openFile-icon);
/*content: var(--toolbarButton-openFile-icon);*/
}
@media (prefers-color-scheme: dark) {
.toolbarButton.openFile::before {
content: url(images/toolbarButton-openFile-dark.svg);
content: var(--toolbarButton-openFile-icon);
/*content: var(--toolbarButton-openFile-icon);*/
}
}
.secondaryToolbarButton.openFile::before {
content: url(images/toolbarButton-openFile.svg);
content: var(--toolbarButton-openFile-icon);
/*content: var(--toolbarButton-openFile-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.openFile::before {
content: url(images/toolbarButton-openFile-dark.svg);
content: var(--toolbarButton-openFile-icon);
/*content: var(--toolbarButton-openFile-icon);*/
}
}
.toolbarButton.download::before {
content: url(images/toolbarButton-download.svg);
content: var(--toolbarButton-download-icon);
/*content: var(--toolbarButton-download-icon);*/
}
@media (prefers-color-scheme: dark) {
.toolbarButton.download::before {
content: url(images/toolbarButton-download-dark.svg);
content: var(--toolbarButton-download-icon);
/*content: var(--toolbarButton-download-icon);*/
}
}
.secondaryToolbarButton.download::before {
content: url(images/toolbarButton-download.svg);
content: var(--toolbarButton-download-icon);
/*content: var(--toolbarButton-download-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.download::before {
content: url(images/toolbarButton-download-dark.svg);
content: var(--toolbarButton-download-icon);
/*content: var(--toolbarButton-download-icon);*/
}
}
@ -2727,53 +2691,53 @@ html[dir="rtl"] .toolbarButton.pageDown::before {
.toolbarButton.bookmark::before {
content: url(images/toolbarButton-bookmark.svg);
content: var(--toolbarButton-bookmark-icon);
/*content: var(--toolbarButton-bookmark-icon);*/
}
@media (prefers-color-scheme: dark) {
.toolbarButton.bookmark::before {
content: url(images/toolbarButton-bookmark-dark.svg);
content: var(--toolbarButton-bookmark-icon);
/*content: var(--toolbarButton-bookmark-icon);*/
}
}
.secondaryToolbarButton.bookmark::before {
content: url(images/toolbarButton-bookmark.svg);
content: var(--toolbarButton-bookmark-icon);
/*content: var(--toolbarButton-bookmark-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.bookmark::before {
content: url(images/toolbarButton-bookmark-dark.svg);
content: var(--toolbarButton-bookmark-icon);
/*content: var(--toolbarButton-bookmark-icon);*/
}
}
#viewThumbnail.toolbarButton::before {
content: url(images/toolbarButton-viewThumbnail.svg);
content: var(--toolbarButton-viewThumbnail-icon);
/*content: var(--toolbarButton-viewThumbnail-icon);*/
}
@media (prefers-color-scheme: dark) {
#viewThumbnail.toolbarButton::before {
content: url(images/toolbarButton-viewThumbnail-dark.svg);
content: var(--toolbarButton-viewThumbnail-icon);
/*content: var(--toolbarButton-viewThumbnail-icon);*/
}
}
#viewOutline.toolbarButton::before {
content: url(images/toolbarButton-viewOutline.svg);
content: var(--toolbarButton-viewOutline-icon);
/*content: var(--toolbarButton-viewOutline-icon);*/
}
@media (prefers-color-scheme: dark) {
#viewOutline.toolbarButton::before {
content: url(images/toolbarButton-viewOutline-dark.svg);
content: var(--toolbarButton-viewOutline-icon);
/*content: var(--toolbarButton-viewOutline-icon);*/
}
}
html[dir="rtl"] #viewOutline.toolbarButton::before {
@ -2782,40 +2746,40 @@ html[dir="rtl"] #viewOutline.toolbarButton::before {
#viewAttachments.toolbarButton::before {
content: url(images/toolbarButton-viewAttachments.svg);
content: var(--toolbarButton-viewAttachments-icon);
/*content: var(--toolbarButton-viewAttachments-icon);*/
}
@media (prefers-color-scheme: dark) {
#viewAttachments.toolbarButton::before {
content: url(images/toolbarButton-viewAttachments-dark.svg);
content: var(--toolbarButton-viewAttachments-icon);
/*content: var(--toolbarButton-viewAttachments-icon);*/
}
}
#viewLayers.toolbarButton::before {
content: url(images/toolbarButton-viewLayers.svg);
content: var(--toolbarButton-viewLayers-icon);
/*content: var(--toolbarButton-viewLayers-icon);*/
}
@media (prefers-color-scheme: dark) {
#viewLayers.toolbarButton::before {
content: url(images/toolbarButton-viewLayers-dark.svg);
content: var(--toolbarButton-viewLayers-icon);
/*content: var(--toolbarButton-viewLayers-icon);*/
}
}
#viewFind.toolbarButton::before {
content: url(images/toolbarButton-search.svg);
content: var(--toolbarButton-search-icon);
/*content: var(--toolbarButton-search-icon);*/
}
@media (prefers-color-scheme: dark) {
#viewFind.toolbarButton::before {
content: url(images/toolbarButton-search-dark.svg);
content: var(--toolbarButton-search-icon);
/*content: var(--toolbarButton-search-icon);*/
}
}
@ -2867,170 +2831,170 @@ html[dir="rtl"] .secondaryToolbarButton > span {
.secondaryToolbarButton.firstPage::before {
content: url(images/secondaryToolbarButton-firstPage.svg);
content: var(--secondaryToolbarButton-firstPage-icon);
/*content: var(--secondaryToolbarButton-firstPage-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.firstPage::before {
content: url(images/secondaryToolbarButton-firstPage-dark.svg);
content: var(--secondaryToolbarButton-firstPage-icon);
/*content: var(--secondaryToolbarButton-firstPage-icon);*/
}
}
.secondaryToolbarButton.lastPage::before {
content: url(images/secondaryToolbarButton-lastPage.svg);
content: var(--secondaryToolbarButton-lastPage-icon);
/*content: var(--secondaryToolbarButton-lastPage-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.lastPage::before {
content: url(images/secondaryToolbarButton-lastPage-dark.svg);
content: var(--secondaryToolbarButton-lastPage-icon);
/*content: var(--secondaryToolbarButton-lastPage-icon);*/
}
}
.secondaryToolbarButton.rotateCcw::before {
content: url(images/secondaryToolbarButton-rotateCcw.svg);
content: var(--secondaryToolbarButton-rotateCcw-icon);
/*content: var(--secondaryToolbarButton-rotateCcw-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.rotateCcw::before {
content: url(images/secondaryToolbarButton-rotateCcw-dark.svg);
content: var(--secondaryToolbarButton-rotateCcw-icon);
/*content: var(--secondaryToolbarButton-rotateCcw-icon);*/
}
}
.secondaryToolbarButton.rotateCw::before {
content: url(images/secondaryToolbarButton-rotateCw.svg);
content: var(--secondaryToolbarButton-rotateCw-icon);
/*content: var(--secondaryToolbarButton-rotateCw-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.rotateCw::before {
content: url(images/secondaryToolbarButton-rotateCw-dark.svg);
content: var(--secondaryToolbarButton-rotateCw-icon);
/*content: var(--secondaryToolbarButton-rotateCw-icon);*/
}
}
.secondaryToolbarButton.selectTool::before {
content: url(images/secondaryToolbarButton-selectTool.svg);
content: var(--secondaryToolbarButton-selectTool-icon);
/*content: var(--secondaryToolbarButton-selectTool-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.selectTool::before {
content: url(images/secondaryToolbarButton-selectTool-dark.svg);
content: var(--secondaryToolbarButton-selectTool-icon);
/*content: var(--secondaryToolbarButton-selectTool-icon);*/
}
}
.secondaryToolbarButton.handTool::before {
content: url(images/secondaryToolbarButton-handTool.svg);
content: var(--secondaryToolbarButton-handTool-icon);
/*content: var(--secondaryToolbarButton-handTool-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.handTool::before {
content: url(images/secondaryToolbarButton-handTool-dark.svg);
content: var(--secondaryToolbarButton-handTool-icon);
/*content: var(--secondaryToolbarButton-handTool-icon);*/
}
}
.secondaryToolbarButton.scrollVertical::before {
content: url(images/secondaryToolbarButton-scrollVertical.svg);
content: var(--secondaryToolbarButton-scrollVertical-icon);
/*content: var(--secondaryToolbarButton-scrollVertical-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.scrollVertical::before {
content: url(images/secondaryToolbarButton-scrollVertical-dark.svg);
content: var(--secondaryToolbarButton-scrollVertical-icon);
/*content: var(--secondaryToolbarButton-scrollVertical-icon);*/
}
}
.secondaryToolbarButton.scrollHorizontal::before {
content: url(images/secondaryToolbarButton-scrollHorizontal.svg);
content: var(--secondaryToolbarButton-scrollHorizontal-icon);
/*content: var(--secondaryToolbarButton-scrollHorizontal-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.scrollHorizontal::before {
content: url(images/secondaryToolbarButton-scrollHorizontal-dark.svg);
content: var(--secondaryToolbarButton-scrollHorizontal-icon);
/*content: var(--secondaryToolbarButton-scrollHorizontal-icon);*/
}
}
.secondaryToolbarButton.scrollWrapped::before {
content: url(images/secondaryToolbarButton-scrollWrapped.svg);
content: var(--secondaryToolbarButton-scrollWrapped-icon);
/*content: var(--secondaryToolbarButton-scrollWrapped-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.scrollWrapped::before {
content: url(images/secondaryToolbarButton-scrollWrapped-dark.svg);
content: var(--secondaryToolbarButton-scrollWrapped-icon);
/*content: var(--secondaryToolbarButton-scrollWrapped-icon);*/
}
}
.secondaryToolbarButton.spreadNone::before {
content: url(images/secondaryToolbarButton-spreadNone.svg);
content: var(--secondaryToolbarButton-spreadNone-icon);
/*content: var(--secondaryToolbarButton-spreadNone-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.spreadNone::before {
content: url(images/secondaryToolbarButton-spreadNone-dark.svg);
content: var(--secondaryToolbarButton-spreadNone-icon);
/*content: var(--secondaryToolbarButton-spreadNone-icon);*/
}
}
.secondaryToolbarButton.spreadOdd::before {
content: url(images/secondaryToolbarButton-spreadOdd.svg);
content: var(--secondaryToolbarButton-spreadOdd-icon);
/*content: var(--secondaryToolbarButton-spreadOdd-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.spreadOdd::before {
content: url(images/secondaryToolbarButton-spreadOdd-dark.svg);
content: var(--secondaryToolbarButton-spreadOdd-icon);
/*content: var(--secondaryToolbarButton-spreadOdd-icon);*/
}
}
.secondaryToolbarButton.spreadEven::before {
content: url(images/secondaryToolbarButton-spreadEven.svg);
content: var(--secondaryToolbarButton-spreadEven-icon);
/*content: var(--secondaryToolbarButton-spreadEven-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.spreadEven::before {
content: url(images/secondaryToolbarButton-spreadEven-dark.svg);
content: var(--secondaryToolbarButton-spreadEven-icon);
/*content: var(--secondaryToolbarButton-spreadEven-icon);*/
}
}
.secondaryToolbarButton.documentProperties::before {
content: url(images/secondaryToolbarButton-documentProperties.svg);
content: var(--secondaryToolbarButton-documentProperties-icon);
/*content: var(--secondaryToolbarButton-documentProperties-icon);*/
}
@media (prefers-color-scheme: dark) {
.secondaryToolbarButton.documentProperties::before {
content: url(images/secondaryToolbarButton-documentProperties-dark.svg);
content: var(--secondaryToolbarButton-documentProperties-icon);
/*content: var(--secondaryToolbarButton-documentProperties-icon);*/
}
}
@ -3140,7 +3104,7 @@ html[dir="rtl"] .toolbarField[type="checkbox"] {
.toolbarField.pageNumber.visiblePageIsLoading {
background-image: url(images/loading.svg);
background-image: var(--loading-icon);
/*background-image: var(--loading-icon);*/
background-repeat: no-repeat;
background-position: 3px;
}
@ -3149,7 +3113,7 @@ html[dir="rtl"] .toolbarField[type="checkbox"] {
.toolbarField.pageNumber.visiblePageIsLoading {
background-image: url(images/loading-dark.svg);
background-image: var(--loading-icon);
/*background-image: var(--loading-icon);*/
}
}
@ -3381,7 +3345,7 @@ html[dir="rtl"] #layersView .treesItem > a > label {
}
.treeItemToggler::before {
content: url(images/treeitem-expanded.svg);
content: var(--treeitem-expanded-icon);
/*content: var(--treeitem-expanded-icon);*/
display: inline-block;
position: absolute;
max-width: 16px;
@ -3390,19 +3354,19 @@ html[dir="rtl"] #layersView .treesItem > a > label {
.treeItemToggler::before {
content: url(images/treeitem-expanded-dark.svg);
content: var(--treeitem-expanded-icon);
/*content: var(--treeitem-expanded-icon);*/
}
}
.treeItemToggler.treeItemsHidden::before {
content: url(images/treeitem-collapsed.svg);
content: var(--treeitem-collapsed-icon);
/*content: var(--treeitem-collapsed-icon);*/
max-width: 16px;
}
@media (prefers-color-scheme: dark) {
.treeItemToggler.treeItemsHidden::before {
content: url(images/treeitem-collapsed-dark.svg);
content: var(--treeitem-collapsed-icon);
/*content: var(--treeitem-collapsed-icon);*/
}
}
html[dir="rtl"] .treeItemToggler.treeItemsHidden::before {

View File

@ -417,3 +417,9 @@ div.log {
white-space: nowrap;
padding: 0.5em;
}
#detailcover { cursor:zoom-in; }
#detailcover:-webkit-full-screen { cursor:zoom-out; }
#detailcover:-moz-full-screen { cursor:zoom-out; }
#detailcover:-ms-fullscreen { cursor:zoom-out; }
#detailcover:fullscreen { cursor:zoom-out; }

View File

@ -1,449 +0,0 @@
/* alphanum.js (C) Brian Huisman
* Based on the Alphanum Algorithm by David Koelle
* The Alphanum Algorithm is discussed at http://www.DaveKoelle.com
*
* Distributed under same license as original
*
* Released under the MIT License - https://opensource.org/licenses/MIT
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
* USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* ********************************************************************
* Alphanum sort() function version - case insensitive
* - Slower, but easier to modify for arrays of objects which contain
* string properties
*
*/
/* exported alphanumCase */
function alphanumCase(a, b) {
function chunkify(t) {
var tz = new Array();
var x = 0, y = -1, n = 0, i, j;
while (i = (j = t.charAt(x++)).charCodeAt(0)) {
var m = (i === 46 || (i >= 48 && i <= 57));
// Compare has to be with != otherwise fails
if (m != n) {
tz[++y] = "";
n = m;
}
tz[y] += j;
}
return tz;
}
var aa = chunkify(a.filename.toLowerCase());
var bb = chunkify(b.filename.toLowerCase());
for (var x = 0; aa[x] && bb[x]; x++) {
if (aa[x] !== bb[x]) {
var c = Number(aa[x]), d = Number(bb[x]);
// Compare has to be with == otherwise fails
if (c == aa[x] && d == bb[x]) {
return c - d;
} else {
return (aa[x] > bb[x]) ? 1 : -1;
}
}
}
return aa.length - bb.length;
}
// ===========================================================================
/**
* archive.js
*
* Provides base functionality for unarchiving.
*
* Licensed under the MIT License
*
* Copyright(c) 2011 Google Inc.
*/
/* global bitjs, Uint8Array */
var bitjs = bitjs || {};
bitjs.archive = bitjs.archive || {};
(function() {
// ===========================================================================
// Stolen from Closure because it's the best way to do Java-like inheritance.
bitjs.base = function(me, optMethodName, varArgs) {
var caller = arguments.callee.caller;
if (caller.superClass_) {
// This is a constructor. Call the superclass constructor.
return caller.superClass_.constructor.apply(
me, Array.prototype.slice.call(arguments, 1));
}
var args = Array.prototype.slice.call(arguments, 2);
var foundCaller = false;
for (var ctor = me.constructor; ctor; ctor = ctor.superClass_ && ctor.superClass_.constructor) {
if (ctor.prototype[optMethodName] === caller) {
foundCaller = true;
} else if (foundCaller) {
return ctor.prototype[optMethodName].apply(me, args);
}
}
// If we did not find the caller in the prototype chain,
// then one of two things happened:
// 1) The caller is an instance method.
// 2) This method was not called by the right caller.
if (me[optMethodName] === caller) {
return me.constructor.prototype[optMethodName].apply(me, args);
} else {
throw Error(
"goog.base called from a method of one name " +
"to a method of a different name");
}
};
bitjs.inherits = function(childCtor, parentCtor) {
/** @constructor */
function TempCtor() {}
TempCtor.prototype = parentCtor.prototype;
childCtor.superClass_ = parentCtor.prototype;
childCtor.prototype = new TempCtor();
childCtor.prototype.constructor = childCtor;
};
// ===========================================================================
/**
* An unarchive event.
*
* @param {string} type The event type.
* @constructor
*/
bitjs.archive.UnarchiveEvent = function(type) {
/**
* The event type.
*
* @type {string}
*/
this.type = type;
};
/**
* The UnarchiveEvent types.
*/
bitjs.archive.UnarchiveEvent.Type = {
START: "start",
PROGRESS: "progress",
EXTRACT: "extract",
FINISH: "finish",
INFO: "info",
ERROR: "error"
};
/**
* Useful for passing info up to the client (for debugging).
*
* @param {string} msg The info message.
*/
bitjs.archive.UnarchiveInfoEvent = function(msg) {
bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.INFO);
/**
* The information message.
*
* @type {string}
*/
this.msg = msg;
};
bitjs.inherits(bitjs.archive.UnarchiveInfoEvent, bitjs.archive.UnarchiveEvent);
/**
* An unrecoverable error has occured.
*
* @param {string} msg The error message.
*/
bitjs.archive.UnarchiveErrorEvent = function(msg) {
bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.ERROR);
/**
* The information message.
*
* @type {string}
*/
this.msg = msg;
};
bitjs.inherits(bitjs.archive.UnarchiveErrorEvent, bitjs.archive.UnarchiveEvent);
/**
* Start event.
*
* @param {string} msg The info message.
*/
bitjs.archive.UnarchiveStartEvent = function() {
bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.START);
};
bitjs.inherits(bitjs.archive.UnarchiveStartEvent, bitjs.archive.UnarchiveEvent);
/**
* Finish event.
*
* @param {string} msg The info message.
*/
bitjs.archive.UnarchiveFinishEvent = function() {
bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.FINISH);
};
bitjs.inherits(bitjs.archive.UnarchiveFinishEvent, bitjs.archive.UnarchiveEvent);
/**
* Progress event.
*/
bitjs.archive.UnarchiveProgressEvent = function(
currentFilename,
currentFileNumber,
currentBytesUnarchivedInFile,
currentBytesUnarchived,
totalUncompressedBytesInArchive,
totalFilesInArchive) {
bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.PROGRESS);
this.currentFilename = currentFilename;
this.currentFileNumber = currentFileNumber;
this.currentBytesUnarchivedInFile = currentBytesUnarchivedInFile;
this.totalFilesInArchive = totalFilesInArchive;
this.currentBytesUnarchived = currentBytesUnarchived;
this.totalUncompressedBytesInArchive = totalUncompressedBytesInArchive;
};
bitjs.inherits(bitjs.archive.UnarchiveProgressEvent, bitjs.archive.UnarchiveEvent);
/**
* All extracted files returned by an Unarchiver will implement
* the following interface:
*
* interface UnarchivedFile {
* string filename
* TypedArray fileData
* }
*
*/
/**
* Extract event.
*/
bitjs.archive.UnarchiveExtractEvent = function(unarchivedFile) {
bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.EXTRACT);
/**
* @type {UnarchivedFile}
*/
this.unarchivedFile = unarchivedFile;
};
bitjs.inherits(bitjs.archive.UnarchiveExtractEvent, bitjs.archive.UnarchiveEvent);
/**
* Base class for all Unarchivers.
*
* @param {ArrayBuffer} arrayBuffer The Array Buffer.
* @param {string} optPathToBitJS Optional string for where the BitJS files are located.
* @constructor
*/
bitjs.archive.Unarchiver = function(arrayBuffer, optPathToBitJS) {
/**
* The ArrayBuffer object.
* @type {ArrayBuffer}
* @protected
*/
this.ab = arrayBuffer;
/**
* The path to the BitJS files.
* @type {string}
* @private
*/
this.pathToBitJS_ = optPathToBitJS || "/";
/**
* A map from event type to an array of listeners.
* @type {Map.<string, Array>}
*/
this.listeners_ = {};
for (var type in bitjs.archive.UnarchiveEvent.Type) {
this.listeners_[bitjs.archive.UnarchiveEvent.Type[type]] = [];
}
};
/**
* Private web worker initialized during start().
* @type {Worker}
* @private
*/
bitjs.archive.Unarchiver.prototype.worker_ = null;
/**
* This method must be overridden by the subclass to return the script filename.
* @return {string} The script filename.
* @protected.
*/
bitjs.archive.Unarchiver.prototype.getScriptFileName = function() {
throw "Subclasses of AbstractUnarchiver must overload getScriptFileName()";
};
/**
* Adds an event listener for UnarchiveEvents.
*
* @param {string} Event type.
* @param {function} An event handler function.
*/
bitjs.archive.Unarchiver.prototype.addEventListener = function(type, listener) {
if (type in this.listeners_) {
if (this.listeners_[type].indexOf(listener) === -1) {
this.listeners_[type].push(listener);
}
}
};
/**
* Removes an event listener.
*
* @param {string} Event type.
* @param {EventListener|function} An event listener or handler function.
*/
bitjs.archive.Unarchiver.prototype.removeEventListener = function(type, listener) {
if (type in this.listeners_) {
var index = this.listeners_[type].indexOf(listener);
if (index !== -1) {
this.listeners_[type].splice(index, 1);
}
}
};
/**
* Receive an event and pass it to the listener functions.
*
* @param {bitjs.archive.UnarchiveEvent} e
* @private
*/
bitjs.archive.Unarchiver.prototype.handleWorkerEvent_ = function(e) {
if ((e instanceof bitjs.archive.UnarchiveEvent || e.type) &&
this.listeners_[e.type] instanceof Array) {
this.listeners_[e.type].forEach(function (listener) {
listener(e);
});
if (e.type === bitjs.archive.UnarchiveEvent.Type.FINISH) {
this.worker_.terminate();
}
}
};
/**
* Starts the unarchive in a separate Web Worker thread and returns immediately.
*/
bitjs.archive.Unarchiver.prototype.start = function() {
var me = this;
var scriptFileName = this.pathToBitJS_ + this.getScriptFileName();
if (scriptFileName) {
this.worker_ = new Worker(scriptFileName);
this.worker_.onerror = function(e) {
throw e;
};
this.worker_.onmessage = function(e) {
if (typeof e.data !== "string") {
// Assume that it is an UnarchiveEvent. Some browsers preserve the 'type'
// so that instanceof UnarchiveEvent returns true, but others do not.
me.handleWorkerEvent_(e.data);
}
};
this.worker_.postMessage({file: this.ab});
}
};
/**
* Terminates the Web Worker for this Unarchiver and returns immediately.
*/
bitjs.archive.Unarchiver.prototype.stop = function() {
if (this.worker_) {
this.worker_.terminate();
}
};
/**
* Unzipper
* @extends {bitjs.archive.Unarchiver}
* @constructor
*/
bitjs.archive.Unzipper = function(arrayBuffer, optPathToBitJS) {
bitjs.base(this, arrayBuffer, optPathToBitJS);
};
bitjs.inherits(bitjs.archive.Unzipper, bitjs.archive.Unarchiver);
bitjs.archive.Unzipper.prototype.getScriptFileName = function() {
return "unzip.js";
};
/**
* Unrarrer
* @extends {bitjs.archive.Unarchiver}
* @constructor
*/
bitjs.archive.Unrarrer = function(arrayBuffer, optPathToBitJS) {
bitjs.base(this, arrayBuffer, optPathToBitJS);
};
bitjs.inherits(bitjs.archive.Unrarrer, bitjs.archive.Unarchiver);
bitjs.archive.Unrarrer.prototype.getScriptFileName = function() {
return "unrar.js";
};
/**
* Untarrer
* @extends {bitjs.archive.Unarchiver}
* @constructor
*/
bitjs.archive.Untarrer = function(arrayBuffer, optPathToBitJS) {
bitjs.base(this, arrayBuffer, optPathToBitJS);
};
bitjs.inherits(bitjs.archive.Untarrer, bitjs.archive.Unarchiver);
bitjs.archive.Untarrer.prototype.getScriptFileName = function() {
return "untar.js";
};
/**
* Factory method that creates an unarchiver based on the byte signature found
* in the arrayBuffer.
* @param {ArrayBuffer} ab
* @param {string=} optPathToBitJS Path to the unarchiver script files.
* @return {bitjs.archive.Unarchiver}
*/
bitjs.archive.GetUnarchiver = function(ab, optPathToBitJS) {
var unarchiver = null;
var pathToBitJS = optPathToBitJS || "";
var h = new Uint8Array(ab, 0, 10);
if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { // Rar!
unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS);
} else if (h[0] === 80 && h[1] === 75) { // PK (Zip)
unarchiver = new bitjs.archive.Unzipper(ab, pathToBitJS);
} else { // Try with tar
unarchiver = new bitjs.archive.Untarrer(ab, pathToBitJS);
}
return unarchiver;
};
})();

View File

@ -1,872 +0,0 @@
/**
* rarvm.js
*
* Licensed under the MIT License
*
* Copyright(c) 2017 Google Inc.
*/
/**
* CRC Implementation.
*/
/* global Uint8Array, Uint32Array, bitjs, DataView, mem */
/* exported MAXWINMASK, UnpackFilter */
function emptyArr(n, v) {
var arr = [];
for (var i = 0; i < n; i += 1) {
arr[i] = v;
}
return arr;
}
var CRCTab = emptyArr(256, 0);
function initCRC() {
for (var i = 0; i < 256; ++i) {
var c = i;
for (var j = 0; j < 8; ++j) {
// Read http://stackoverflow.com/questions/6798111/bitwise-operations-on-32-bit-unsigned-ints
// for the bitwise operator issue (JS interprets operands as 32-bit signed
// integers and we need to deal with unsigned ones here).
c = ((c & 1) ? ((c >>> 1) ^ 0xEDB88320) : (c >>> 1)) >>> 0;
}
CRCTab[i] = c;
}
}
/**
* @param {number} startCRC
* @param {Uint8Array} arr
* @return {number}
*/
function CRC(startCRC, arr) {
if (CRCTab[1] === 0) {
initCRC();
}
/*
#if defined(LITTLE_ENDIAN) && defined(PRESENT_INT32) && defined(ALLOW_NOT_ALIGNED_INT)
while (Size>0 && ((long)Data & 7))
{
StartCRC=CRCTab[(byte)(StartCRC^Data[0])]^(StartCRC>>8);
Size--;
Data++;
}
while (Size>=8)
{
StartCRC^=*(uint32 *)Data;
StartCRC=CRCTab[(byte)StartCRC]^(StartCRC>>8);
StartCRC=CRCTab[(byte)StartCRC]^(StartCRC>>8);
StartCRC=CRCTab[(byte)StartCRC]^(StartCRC>>8);
StartCRC=CRCTab[(byte)StartCRC]^(StartCRC>>8);
StartCRC^=*(uint32 *)(Data+4);
StartCRC=CRCTab[(byte)StartCRC]^(StartCRC>>8);
StartCRC=CRCTab[(byte)StartCRC]^(StartCRC>>8);
StartCRC=CRCTab[(byte)StartCRC]^(StartCRC>>8);
StartCRC=CRCTab[(byte)StartCRC]^(StartCRC>>8);
Data+=8;
Size-=8;
}
#endif
*/
for (var i = 0; i < arr.length; ++i) {
var byte = ((startCRC ^ arr[i]) >>> 0) & 0xff;
startCRC = (CRCTab[byte] ^ (startCRC >>> 8)) >>> 0;
}
return startCRC;
}
// ============================================================================================== //
/**
* RarVM Implementation.
*/
var VM_MEMSIZE = 0x40000;
var VM_MEMMASK = (VM_MEMSIZE - 1);
var VM_GLOBALMEMADDR = 0x3C000;
var VM_GLOBALMEMSIZE = 0x2000;
var VM_FIXEDGLOBALSIZE = 64;
var MAXWINSIZE = 0x400000;
var MAXWINMASK = (MAXWINSIZE - 1);
/**
*/
var VmCommands = {
VM_MOV: 0,
VM_CMP: 1,
VM_ADD: 2,
VM_SUB: 3,
VM_JZ: 4,
VM_JNZ: 5,
VM_INC: 6,
VM_DEC: 7,
VM_JMP: 8,
VM_XOR: 9,
VM_AND: 10,
VM_OR: 11,
VM_TEST: 12,
VM_JS: 13,
VM_JNS: 14,
VM_JB: 15,
VM_JBE: 16,
VM_JA: 17,
VM_JAE: 18,
VM_PUSH: 19,
VM_POP: 20,
VM_CALL: 21,
VM_RET: 22,
VM_NOT: 23,
VM_SHL: 24,
VM_SHR: 25,
VM_SAR: 26,
VM_NEG: 27,
VM_PUSHA: 28,
VM_POPA: 29,
VM_PUSHF: 30,
VM_POPF: 31,
VM_MOVZX: 32,
VM_MOVSX: 33,
VM_XCHG: 34,
VM_MUL: 35,
VM_DIV: 36,
VM_ADC: 37,
VM_SBB: 38,
VM_PRINT: 39,
/*
#ifdef VM_OPTIMIZE
VM_MOVB, VM_MOVD, VM_CMPB, VM_CMPD,
VM_ADDB, VM_ADDD, VM_SUBB, VM_SUBD, VM_INCB, VM_INCD, VM_DECB, VM_DECD,
VM_NEGB, VM_NEGD,
#endif
*/
// TODO: This enum value would be much larger if VM_OPTIMIZE.
VM_STANDARD: 40,
};
/**
*/
var VmStandardFilters = {
VMSF_NONE: 0,
VMSF_E8: 1,
VMSF_E8E9: 2,
VMSF_ITANIUM: 3,
VMSF_RGB: 4,
VMSF_AUDIO: 5,
VMSF_DELTA: 6,
VMSF_UPCASE: 7,
};
/**
*/
var VmFlags = {
VM_FC: 1,
VM_FZ: 2,
VM_FS: 0x80000000,
};
/**
*/
var VmOpType = {
VM_OPREG: 0,
VM_OPINT: 1,
VM_OPREGMEM: 2,
VM_OPNONE: 3,
};
/**
* Finds the key that maps to a given value in an object. This function is useful in debugging
* variables that use the above enums.
* @param {Object} obj
* @param {number} val
* @return {string} The key/enum value as a string.
*/
function findKeyForValue(obj, val) {
for (var key in obj) {
if (obj[key] === val) {
return key;
}
}
return null;
}
function getDebugString(obj, val) {
var s = "Unknown.";
if (obj === VmCommands) {
s = "VmCommands.";
} else if (obj === VmStandardFilters) {
s = "VmStandardFilters.";
} else if (obj === VmFlags) {
s = "VmOpType.";
} else if (obj === VmOpType) {
s = "VmOpType.";
}
return s + findKeyForValue(obj, val);
}
/**
* @struct
* @constructor
*/
var VmPreparedOperand = function() {
/** @type {VmOpType} */
this.Type;
/** @type {number} */
this.Data = 0;
/** @type {number} */
this.Base = 0;
// TODO: In C++ this is a uint*
/** @type {Array<number>} */
this.Addr = null;
};
/** @return {string} */
VmPreparedOperand.prototype.toString = function() {
if (this.Type === null) {
return "Error: Type was null in VmPreparedOperand";
}
return "{ " +
"Type: " + getDebugString(VmOpType, this.Type) +
", Data: " + this.Data +
", Base: " + this.Base +
" }";
};
/**
* @struct
* @constructor
*/
var VmPreparedCommand = function() {
/** @type {VmCommands} */
this.OpCode;
/** @type {boolean} */
this.ByteMode = false;
/** @type {VmPreparedOperand} */
this.Op1 = new VmPreparedOperand();
/** @type {VmPreparedOperand} */
this.Op2 = new VmPreparedOperand();
};
/** @return {string} */
VmPreparedCommand.prototype.toString = function(indent) {
if (this.OpCode === null) {
return "Error: OpCode was null in VmPreparedCommand";
}
indent = indent || "";
return indent + "{\n" +
indent + " OpCode: " + getDebugString(VmCommands, this.OpCode) + ",\n" +
indent + " ByteMode: " + this.ByteMode + ",\n" +
indent + " Op1: " + this.Op1.toString() + ",\n" +
indent + " Op2: " + this.Op2.toString() + ",\n" +
indent + "}";
};
/**
* @struct
* @constructor
*/
var VmPreparedProgram = function() {
/** @type {Array<VmPreparedCommand>} */
this.Cmd = [];
/** @type {Array<VmPreparedCommand>} */
this.AltCmd = null;
/** @type {Uint8Array} */
this.GlobalData = new Uint8Array();
/** @type {Uint8Array} */
this.StaticData = new Uint8Array(); // static data contained in DB operators
/** @type {Uint32Array} */
this.InitR = new Uint32Array(7);
/**
* A pointer to bytes that have been filtered by a program.
* @type {Uint8Array}
*/
this.FilteredData = null;
};
/** @return {string} */
VmPreparedProgram.prototype.toString = function() {
var s = "{\n Cmd: [\n";
for (var i = 0; i < this.Cmd.length; ++i) {
s += this.Cmd[i].toString(" ") + ",\n";
}
s += "],\n";
// TODO: Dump GlobalData, StaticData, InitR?
s += " }\n";
return s;
};
/**
* @struct
* @constructor
*/
var UnpackFilter = function() {
/** @type {number} */
this.BlockStart = 0;
/** @type {number} */
this.BlockLength = 0;
/** @type {number} */
this.ExecCount = 0;
/** @type {boolean} */
this.NextWindow = false;
// position of parent filter in Filters array used as prototype for filter
// in PrgStack array. Not defined for filters in Filters array.
/** @type {number} */
this.ParentFilter = null;
/** @type {VmPreparedProgram} */
this.Prg = new VmPreparedProgram();
};
var VMCF_OP0 = 0;
var VMCF_OP1 = 1;
var VMCF_OP2 = 2;
var VMCF_OPMASK = 3;
var VMCF_BYTEMODE = 4;
var VMCF_JUMP = 8;
var VMCF_PROC = 16;
var VMCF_USEFLAGS = 32;
var VMCF_CHFLAGS = 64;
var VmCmdFlags = [
/* VM_MOV */
VMCF_OP2 | VMCF_BYTEMODE,
/* VM_CMP */
VMCF_OP2 | VMCF_BYTEMODE | VMCF_CHFLAGS,
/* VM_ADD */
VMCF_OP2 | VMCF_BYTEMODE | VMCF_CHFLAGS,
/* VM_SUB */
VMCF_OP2 | VMCF_BYTEMODE | VMCF_CHFLAGS,
/* VM_JZ */
VMCF_OP1 | VMCF_JUMP | VMCF_USEFLAGS,
/* VM_JNZ */
VMCF_OP1 | VMCF_JUMP | VMCF_USEFLAGS,
/* VM_INC */
VMCF_OP1 | VMCF_BYTEMODE | VMCF_CHFLAGS,
/* VM_DEC */
VMCF_OP1 | VMCF_BYTEMODE | VMCF_CHFLAGS,
/* VM_JMP */
VMCF_OP1 | VMCF_JUMP,
/* VM_XOR */
VMCF_OP2 | VMCF_BYTEMODE | VMCF_CHFLAGS,
/* VM_AND */
VMCF_OP2 | VMCF_BYTEMODE | VMCF_CHFLAGS,
/* VM_OR */
VMCF_OP2 | VMCF_BYTEMODE | VMCF_CHFLAGS,
/* VM_TEST */
VMCF_OP2 | VMCF_BYTEMODE | VMCF_CHFLAGS,
/* VM_JS */
VMCF_OP1 | VMCF_JUMP | VMCF_USEFLAGS,
/* VM_JNS */
VMCF_OP1 | VMCF_JUMP | VMCF_USEFLAGS,
/* VM_JB */
VMCF_OP1 | VMCF_JUMP | VMCF_USEFLAGS,
/* VM_JBE */
VMCF_OP1 | VMCF_JUMP | VMCF_USEFLAGS,
/* VM_JA */
VMCF_OP1 | VMCF_JUMP | VMCF_USEFLAGS,
/* VM_JAE */
VMCF_OP1 | VMCF_JUMP | VMCF_USEFLAGS,
/* VM_PUSH */
VMCF_OP1,
/* VM_POP */
VMCF_OP1,
/* VM_CALL */
VMCF_OP1 | VMCF_PROC,
/* VM_RET */
VMCF_OP0 | VMCF_PROC,
/* VM_NOT */
VMCF_OP1 | VMCF_BYTEMODE,
/* VM_SHL */
VMCF_OP2 | VMCF_BYTEMODE | VMCF_CHFLAGS,
/* VM_SHR */
VMCF_OP2 | VMCF_BYTEMODE | VMCF_CHFLAGS,
/* VM_SAR */
VMCF_OP2 | VMCF_BYTEMODE | VMCF_CHFLAGS,
/* VM_NEG */
VMCF_OP1 | VMCF_BYTEMODE | VMCF_CHFLAGS,
/* VM_PUSHA */
VMCF_OP0,
/* VM_POPA */
VMCF_OP0,
/* VM_PUSHF */
VMCF_OP0 | VMCF_USEFLAGS,
/* VM_POPF */
VMCF_OP0 | VMCF_CHFLAGS,
/* VM_MOVZX */
VMCF_OP2,
/* VM_MOVSX */
VMCF_OP2,
/* VM_XCHG */
VMCF_OP2 | VMCF_BYTEMODE,
/* VM_MUL */
VMCF_OP2 | VMCF_BYTEMODE,
/* VM_DIV */
VMCF_OP2 | VMCF_BYTEMODE,
/* VM_ADC */
VMCF_OP2 | VMCF_BYTEMODE | VMCF_USEFLAGS | VMCF_CHFLAGS,
/* VM_SBB */
VMCF_OP2 | VMCF_BYTEMODE | VMCF_USEFLAGS | VMCF_CHFLAGS,
/* VM_PRINT */
VMCF_OP0,
];
/**
* @param {number} length
* @param {number} crc
* @param {VmStandardFilters} type
* @struct
* @constructor
*/
var StandardFilterSignature = function(length, crc, type) {
/** @type {number} */
this.Length = length;
/** @type {number} */
this.CRC = crc;
/** @type {VmStandardFilters} */
this.Type = type;
};
/**
* @type {Array<StandardFilterSignature>}
*/
var StdList = [
new StandardFilterSignature(53, 0xad576887, VmStandardFilters.VMSF_E8),
new StandardFilterSignature(57, 0x3cd7e57e, VmStandardFilters.VMSF_E8E9),
new StandardFilterSignature(120, 0x3769893f, VmStandardFilters.VMSF_ITANIUM),
new StandardFilterSignature(29, 0x0e06077d, VmStandardFilters.VMSF_DELTA),
new StandardFilterSignature(149, 0x1c2c5dc8, VmStandardFilters.VMSF_RGB),
new StandardFilterSignature(216, 0xbc85e701, VmStandardFilters.VMSF_AUDIO),
new StandardFilterSignature(40, 0x46b9c560, VmStandardFilters.VMSF_UPCASE),
];
/**
* @constructor
*/
var RarVM = function() {
/** @private {Uint8Array} */
this.mem_ = null;
/** @private {Uint32Array<number>} */
this.R_ = new Uint32Array(8);
/** @private {number} */
this.flags_ = 0;
};
/**
* Initializes the memory of the VM.
*/
RarVM.prototype.init = function() {
if (!this.mem_) {
this.mem_ = new Uint8Array(VM_MEMSIZE);
}
};
/**
* @param {Uint8Array} code
* @return {VmStandardFilters}
*/
RarVM.prototype.isStandardFilter = function(code) {
var codeCRC = (CRC(0xffffffff, code, code.length) ^ 0xffffffff) >>> 0;
for (var i = 0; i < StdList.length; ++i) {
if (StdList[i].CRC === codeCRC && StdList[i].Length === code.length) {
return StdList[i].Type;
}
}
return VmStandardFilters.VMSF_NONE;
};
/**
* @param {VmPreparedOperand} op
* @param {boolean} byteMode
* @param {bitjs.io.BitStream} bstream A rtl bit stream.
*/
RarVM.prototype.decodeArg = function(op, byteMode, bstream) {
var data = bstream.peekBits(16);
if (data & 0x8000) {
op.Type = VmOpType.VM_OPREG; // Operand is register (R[0]..R[7])
bstream.readBits(1); // 1 flag bit and...
op.Data = bstream.readBits(3); // ... 3 register number bits
op.Addr = [this.R_[op.Data]]; // TODO &R[Op.Data] // Register address
} else {
if ((data & 0xc000) === 0) {
op.Type = VmOpType.VM_OPINT; // Operand is integer
bstream.readBits(2); // 2 flag bits
if (byteMode) {
op.Data = bstream.readBits(8); // Byte integer.
} else {
op.Data = RarVM.readData(bstream); // 32 bit integer.
}
} else {
// Operand is data addressed by register data, base address or both.
op.Type = VmOpType.VM_OPREGMEM;
if ((data & 0x2000) === 0) {
bstream.readBits(3); // 3 flag bits
// Base address is zero, just use the address from register.
op.Data = bstream.readBits(3); // (Data>>10)&7
op.Addr = [this.R_[op.Data]]; // TODO &R[op.Data]
op.Base = 0;
} else {
bstream.readBits(4); // 4 flag bits
if ((data & 0x1000) === 0) {
// Use both register and base address.
op.Data = bstream.readBits(3);
op.Addr = [this.R_[op.Data]]; // TODO &R[op.Data]
} else {
// Use base address only. Access memory by fixed address.
op.Data = 0;
}
op.Base = RarVM.readData(bstream); // Read base address.
}
}
}
};
/**
* @param {VmPreparedProgram} prg
*/
RarVM.prototype.execute = function(prg) {
this.R_.set(prg.InitR);
var globalSize = Math.min(prg.GlobalData.length, VM_GLOBALMEMSIZE);
if (globalSize) {
this.mem_.set(prg.GlobalData.subarray(0, globalSize), VM_GLOBALMEMADDR);
}
var staticSize = Math.min(prg.StaticData.length, VM_GLOBALMEMSIZE - globalSize);
if (staticSize) {
this.mem_.set(prg.StaticData.subarray(0, staticSize), VM_GLOBALMEMADDR + globalSize);
}
this.R_[7] = VM_MEMSIZE;
this.flags_ = 0;
var preparedCodes = prg.AltCmd ? prg.AltCmd : prg.Cmd;
if (prg.Cmd.length > 0 && !this.executeCode(preparedCodes)) {
// Invalid VM program. Let's replace it with 'return' command.
preparedCodes.OpCode = VmCommands.VM_RET;
}
var dataView = new DataView(this.mem_.buffer, VM_GLOBALMEMADDR);
var newBlockPos = dataView.getUint32(0x20, true /* little endian */ ) & VM_MEMMASK;
var newBlockSize = dataView.getUint32(0x1c, true /* little endian */ ) & VM_MEMMASK;
if (newBlockPos + newBlockSize >= VM_MEMSIZE) {
newBlockPos = newBlockSize = 0;
}
prg.FilteredData = this.mem_.subarray(newBlockPos, newBlockPos + newBlockSize);
prg.GlobalData = new Uint8Array(0);
var dataSize = Math.min(dataView.getUint32(0x30),
(VM_GLOBALMEMSIZE - VM_FIXEDGLOBALSIZE));
if (dataSize !== 0) {
var len = dataSize + VM_FIXEDGLOBALSIZE;
prg.GlobalData = new Uint8Array(len);
prg.GlobalData.set(mem.subarray(VM_GLOBALMEMADDR, VM_GLOBALMEMADDR + len));
}
};
/**
* @param {Array<VmPreparedCommand>} preparedCodes
* @return {boolean}
*/
RarVM.prototype.executeCode = function(preparedCodes) {
var codeIndex = 0;
var cmd = preparedCodes[codeIndex];
// TODO: Why is this an infinite loop instead of just returning
// when a VM_RET is hit?
while (1) {
switch (cmd.OpCode) {
case VmCommands.VM_RET:
if (this.R_[7] >= VM_MEMSIZE) {
return true;
}
//SET_IP(GET_VALUE(false,(uint *)&Mem[R[7] & VM_MEMMASK]));
this.R_[7] += 4;
continue;
case VmCommands.VM_STANDARD:
this.executeStandardFilter(cmd.Op1.Data);
break;
default:
console.error("RarVM OpCode not supported: " + getDebugString(VmCommands, cmd.OpCode));
break;
} // switch (cmd.OpCode)
codeIndex++;
cmd = preparedCodes[codeIndex];
}
};
/**
* @param {number} filterType
*/
RarVM.prototype.executeStandardFilter = function(filterType) {
switch (filterType) {
case VmStandardFilters.VMSF_DELTA:
var dataSize = this.R_[4];
var channels = this.R_[0];
var srcPos = 0;
var border = dataSize * 2;
//SET_VALUE(false,&Mem[VM_GLOBALMEMADDR+0x20],DataSize);
var dataView = new DataView(this.mem_.buffer, VM_GLOBALMEMADDR);
dataView.setUint32(0x20, dataSize, true /* little endian */ );
if (dataSize >= VM_GLOBALMEMADDR / 2) {
break;
}
// Bytes from same channels are grouped to continual data blocks,
// so we need to place them back to their interleaving positions.
for (var curChannel = 0; curChannel < channels; ++curChannel) {
var prevByte = 0;
for (var destPos = dataSize + curChannel; destPos < border; destPos += channels) {
prevByte = (prevByte - this.mem_[srcPos++]) & 0xff;
this.mem_[destPos] = prevByte;
}
}
break;
default:
console.error("RarVM Standard Filter not supported: " + getDebugString(VmStandardFilters, filterType));
break;
}
};
/**
* @param {Uint8Array} code
* @param {VmPreparedProgram} prg
*/
RarVM.prototype.prepare = function(code, prg) {
var codeSize = code.length;
var i;
var curCmd;
//InitBitInput();
//memcpy(InBuf,Code,Min(CodeSize,BitInput::MAX_SIZE));
var bstream = new bitjs.io.BitStream(code.buffer, true /* rtl */ );
// Calculate the single byte XOR checksum to check validity of VM code.
var xorSum = 0;
for (i = 1; i < codeSize; ++i) {
xorSum ^= code[i];
}
bstream.readBits(8);
prg.Cmd = []; // TODO: Is this right? I don't see it being done in rarvm.cpp.
// VM code is valid if equal.
if (xorSum === code[0]) {
var filterType = this.isStandardFilter(code);
if (filterType !== VmStandardFilters.VMSF_NONE) {
// VM code is found among standard filters.
curCmd = new VmPreparedCommand();
prg.Cmd.push(curCmd);
curCmd.OpCode = VmCommands.VM_STANDARD;
curCmd.Op1.Data = filterType;
// TODO: Addr=&CurCmd->Op1.Data
curCmd.Op1.Addr = [curCmd.Op1.Data];
curCmd.Op2.Addr = [null]; // &CurCmd->Op2.Data;
curCmd.Op1.Type = VmOpType.VM_OPNONE;
curCmd.Op2.Type = VmOpType.VM_OPNONE;
codeSize = 0;
}
var dataFlag = bstream.readBits(1);
// Read static data contained in DB operators. This data cannot be
// changed, it is a part of VM code, not a filter parameter.
if (dataFlag & 0x8000) {
var dataSize = RarVM.readData(bstream) + 1;
// TODO: This accesses the byte pointer of the bstream directly. Is that ok?
for (i = 0; i < bstream.bytePtr < codeSize && i < dataSize; ++i) {
// Append a byte to the program's static data.
var newStaticData = new Uint8Array(prg.StaticData.length + 1);
newStaticData.set(prg.StaticData);
newStaticData[newStaticData.length - 1] = bstream.readBits(8);
prg.StaticData = newStaticData;
}
}
while (bstream.bytePtr < codeSize) {
curCmd = new VmPreparedCommand();
prg.Cmd.push(curCmd); // Prg->Cmd.Add(1)
var flag = bstream.peekBits(1);
if (!flag) { // (Data&0x8000)==0
curCmd.OpCode = bstream.readBits(4);
} else {
curCmd.OpCode = (bstream.readBits(6) - 24);
}
if (VmCmdFlags[curCmd.OpCode] & VMCF_BYTEMODE) {
curCmd.ByteMode = (bstream.readBits(1) !== 0);
} else {
curCmd.ByteMode = 0;
}
curCmd.Op1.Type = VmOpType.VM_OPNONE;
curCmd.Op2.Type = VmOpType.VM_OPNONE;
var opNum = (VmCmdFlags[curCmd.OpCode] & VMCF_OPMASK);
curCmd.Op1.Addr = null;
curCmd.Op2.Addr = null;
if (opNum > 0) {
this.decodeArg(curCmd.Op1, curCmd.ByteMode, bstream); // reading the first operand
if (opNum === 2) {
this.decodeArg(curCmd.Op2, curCmd.ByteMode, bstream); // reading the second operand
} else {
if (curCmd.Op1.Type === VmOpType.VM_OPINT && (VmCmdFlags[curCmd.OpCode] & (VMCF_JUMP | VMCF_PROC))) {
// Calculating jump distance.
var distance = curCmd.Op1.Data;
if (distance >= 256) {
distance -= 256;
} else {
if (distance >= 136) {
distance -= 264;
} else {
if (distance >= 16) {
distance -= 8;
} else {
if (distance >= 8) {
distance -= 16;
}
}
}
distance += prg.Cmd.length;
}
curCmd.Op1.Data = distance;
}
}
} // if (OpNum>0)
} // while ((uint)InAddr<CodeSize)
} // if (XorSum==Code[0])
curCmd = new VmPreparedCommand();
prg.Cmd.push(curCmd);
curCmd.OpCode = VmCommands.VM_RET;
// TODO: Addr=&CurCmd->Op1.Data
curCmd.Op1.Addr = [curCmd.Op1.Data];
curCmd.Op2.Addr = [curCmd.Op2.Data];
curCmd.Op1.Type = VmOpType.VM_OPNONE;
curCmd.Op2.Type = VmOpType.VM_OPNONE;
// If operand 'Addr' field has not been set by DecodeArg calls above,
// let's set it to point to operand 'Data' field. It is necessary for
// VM_OPINT type operands (usual integers) or maybe if something was
// not set properly for other operands. 'Addr' field is required
// for quicker addressing of operand data.
for (i = 0; i < prg.Cmd.length; ++i) {
var cmd = prg.Cmd[i];
if (cmd.Op1.Addr === null) {
cmd.Op1.Addr = [cmd.Op1.Data];
}
if (cmd.Op2.Addr === null) {
cmd.Op2.Addr = [cmd.Op2.Data];
}
}
/*
#ifdef VM_OPTIMIZE
if (CodeSize!=0)
Optimize(Prg);
#endif
*/
};
/**
* @param {Uint8Array} arr The byte array to set a value in.
* @param {number} value The unsigned 32-bit value to set.
* @param {number} offset Offset into arr to start setting the value, defaults to 0.
*/
RarVM.prototype.setLowEndianValue = function(arr, value, offset) {
var i = offset || 0;
arr[i] = value & 0xff;
arr[i + 1] = (value >>> 8) & 0xff;
arr[i + 2] = (value >>> 16) & 0xff;
arr[i + 3] = (value >>> 24) & 0xff;
};
/**
* Sets a number of bytes of the VM memory at the given position from a
* source buffer of bytes.
* @param {number} pos The position in the VM memory to start writing to.
* @param {Uint8Array} buffer The source buffer of bytes.
* @param {number} dataSize The number of bytes to set.
*/
RarVM.prototype.setMemory = function(pos, buffer, dataSize) {
if (pos < VM_MEMSIZE) {
var numBytes = Math.min(dataSize, VM_MEMSIZE - pos);
for (var i = 0; i < numBytes; ++i) {
this.mem_[pos + i] = buffer[i];
}
}
};
/**
* Static function that reads in the next set of bits for the VM
* (might return 4, 8, 16 or 32 bits).
* @param {bitjs.io.BitStream} bstream A RTL bit stream.
* @return {number} The value of the bits read.
*/
RarVM.readData = function(bstream) {
// Read in the first 2 bits.
var flags = bstream.readBits(2);
switch (flags) { // Data&0xc000
// Return the next 4 bits.
case 0:
return bstream.readBits(4); // (Data>>10)&0xf
case 1: // 0x4000
// 0x3c00 => 0011 1100 0000 0000
if (bstream.peekBits(4) === 0) { // (Data&0x3c00)==0
// Skip the 4 zero bits.
bstream.readBits(4);
// Read in the next 8 and pad with 1s to 32 bits.
return (0xffffff00 | bstream.readBits(8)) >>> 0; // ((Data>>2)&0xff)
}
// Else, read in the next 8.
return bstream.readBits(8);
// Read in the next 16.
case 2: // 0x8000
var val = bstream.getBits();
bstream.readBits(16);
return val; //bstream.readBits(16);
// case 3
default:
return (bstream.readBits(16) << 16) | bstream.readBits(16);
}
};
// ============================================================================================== //

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,179 +0,0 @@
/**
* untar.js
*
* Licensed under the MIT License
*
* Copyright(c) 2011 Google Inc.
*
* Reference Documentation:
*
* TAR format: http://www.gnu.org/software/automake/manual/tar/Standard.html
*/
/* global bitjs, importScripts, Uint8Array */
// This file expects to be invoked as a Worker (see onmessage below).
importScripts("../io/bytestream.js");
importScripts("archive.js");
// Progress variables.
var currentFilename = "";
var currentFileNumber = 0;
var currentBytesUnarchivedInFile = 0;
var currentBytesUnarchived = 0;
var totalUncompressedBytesInArchive = 0;
var totalFilesInArchive = 0;
var allLocalFiles = [];
// Helper functions.
var info = function(str) {
postMessage(new bitjs.archive.UnarchiveInfoEvent(str));
};
var err = function(str) {
postMessage(new bitjs.archive.UnarchiveErrorEvent(str));
};
// Removes all characters from the first zero-byte in the string onwards.
var readCleanString = function(bstr, numBytes) {
var str = bstr.readString(numBytes);
var zIndex = str.indexOf(String.fromCharCode(0));
return zIndex != -1 ? str.substr(0, zIndex) : str;
};
var postProgress = function() {
postMessage(new bitjs.archive.UnarchiveProgressEvent(
currentFilename,
currentFileNumber,
currentBytesUnarchivedInFile,
currentBytesUnarchived,
totalUncompressedBytesInArchive,
totalFilesInArchive
));
};
// takes a ByteStream and parses out the local file information
var TarLocalFile = function(bstream) {
this.isValid = false;
var bytesRead = 0;
// Read in the header block
this.name = readCleanString(bstream, 100);
this.mode = readCleanString(bstream, 8);
this.uid = readCleanString(bstream, 8);
this.gid = readCleanString(bstream, 8);
this.size = parseInt(readCleanString(bstream, 12), 8);
this.mtime = readCleanString(bstream, 12);
this.chksum = readCleanString(bstream, 8);
this.typeflag = readCleanString(bstream, 1);
this.linkname = readCleanString(bstream, 100);
this.maybeMagic = readCleanString(bstream, 6);
if (this.maybeMagic === "ustar") {
this.version = readCleanString(bstream, 2);
this.uname = readCleanString(bstream, 32);
this.gname = readCleanString(bstream, 32);
this.devmajor = readCleanString(bstream, 8);
this.devminor = readCleanString(bstream, 8);
this.prefix = readCleanString(bstream, 155);
if (this.prefix.length) {
this.name = this.prefix + this.name;
}
bstream.readBytes(12); // 512 - 500
} else {
bstream.readBytes(255); // 512 - 257
}
bytesRead += 512;
// Done header, now rest of blocks are the file contents.
this.filename = this.name;
this.fileData = null;
info("Untarring file '" + this.filename + "'");
info(" size = " + this.size);
info(" typeflag = " + this.typeflag);
// A regular file.
if (this.typeflag == 0) {
info(" This is a regular file.");
var sizeInBytes = parseInt(this.size);
this.fileData = new Uint8Array(bstream.readBytes(sizeInBytes));
bytesRead += sizeInBytes;
if (this.name.length > 0 && this.size > 0 && this.fileData && this.fileData.buffer) {
this.isValid = true;
}
// Round up to 512-byte blocks.
var remaining = 512 - (bytesRead % 512);
if (remaining > 0 && remaining < 512) {
bstream.readBytes(remaining);
}
} else if (this.typeflag == 5) {
info(" This is a directory.");
}
};
var untar = function(arrayBuffer) {
postMessage(new bitjs.archive.UnarchiveStartEvent());
currentFilename = "";
currentFileNumber = 0;
currentBytesUnarchivedInFile = 0;
currentBytesUnarchived = 0;
totalUncompressedBytesInArchive = 0;
totalFilesInArchive = 0;
allLocalFiles = [];
var bstream = new bitjs.io.ByteStream(arrayBuffer);
postProgress();
/*
// go through whole file, read header of each block and memorize, filepointer
*/
while (bstream.peekNumber(4) !== 0) {
var localFile = new TarLocalFile(bstream);
allLocalFiles.push(localFile);
postProgress();
}
// got all local files, now sort them
allLocalFiles.sort(alphanumCase);
allLocalFiles.forEach(function(oneLocalFile) {
// While we don't encounter an empty block, keep making TarLocalFiles.
if (oneLocalFile && oneLocalFile.isValid) {
// If we make it to this point and haven't thrown an error, we have successfully
// read in the data for a local file, so we can update the actual bytestream.
totalUncompressedBytesInArchive += oneLocalFile.size;
// update progress
currentFilename = oneLocalFile.filename;
currentFileNumber = totalFilesInArchive++;
currentBytesUnarchivedInFile = oneLocalFile.size;
currentBytesUnarchived += oneLocalFile.size;
postMessage(new bitjs.archive.UnarchiveExtractEvent(oneLocalFile));
postProgress();
}
});
totalFilesInArchive = allLocalFiles.length;
postProgress();
postMessage(new bitjs.archive.UnarchiveFinishEvent());
};
// event.data.file has the first ArrayBuffer.
// event.data.bytes has all subsequent ArrayBuffers.
onmessage = function(event) {
try {
untar(event.data.file, true);
} catch (e) {
if (typeof e === "string" && e.startsWith("Error! Overflowed")) {
// Overrun the buffer.
// unarchiveState = UnarchiveState.WAITING;
} else {
err("Found an error while untarring");
err(e);
throw e;
}
}
};

View File

@ -1,660 +0,0 @@
/**
* unzip.js
*
* Licensed under the MIT License
*
* Copyright(c) 2011 Google Inc.
* Copyright(c) 2011 antimatter15
*
* Reference Documentation:
*
* ZIP format: http://www.pkware.com/documents/casestudies/APPNOTE.TXT
* DEFLATE format: http://tools.ietf.org/html/rfc1951
*/
/* global bitjs, importScripts, Uint8Array*/
// This file expects to be invoked as a Worker (see onmessage below).
importScripts("../io/bitstream.js");
importScripts("../io/bytebuffer.js");
importScripts("../io/bytestream.js");
importScripts("archive.js");
// Progress variables.
var currentFilename = "";
var currentFileNumber = 0;
var currentBytesUnarchivedInFile = 0;
var currentBytesUnarchived = 0;
var totalUncompressedBytesInArchive = 0;
var totalFilesInArchive = 0;
// Helper functions.
var info = function(str) {
postMessage(new bitjs.archive.UnarchiveInfoEvent(str));
};
var err = function(str) {
postMessage(new bitjs.archive.UnarchiveErrorEvent(str));
};
var postProgress = function() {
postMessage(new bitjs.archive.UnarchiveProgressEvent(
currentFilename,
currentFileNumber,
currentBytesUnarchivedInFile,
currentBytesUnarchived,
totalUncompressedBytesInArchive,
totalFilesInArchive));
};
var zLocalFileHeaderSignature = 0x04034b50;
var zArchiveExtraDataSignature = 0x08064b50;
var zCentralFileHeaderSignature = 0x02014b50;
var zDigitalSignatureSignature = 0x05054b50;
// takes a ByteStream and parses out the local file information
var ZipLocalFile = function(bstream) {
if (typeof bstream !== typeof {} || !bstream.readNumber || typeof bstream.readNumber !== typeof function() {}) {
return null;
}
bstream.readNumber(4); // swallow signature
this.version = bstream.readNumber(2);
this.generalPurpose = bstream.readNumber(2);
this.compressionMethod = bstream.readNumber(2);
this.lastModFileTime = bstream.readNumber(2);
this.lastModFileDate = bstream.readNumber(2);
this.crc32 = bstream.readNumber(4);
this.compressedSize = bstream.readNumber(4);
this.uncompressedSize = bstream.readNumber(4);
this.fileNameLength = bstream.readNumber(2);
this.extraFieldLength = bstream.readNumber(2);
this.filename = null;
if (this.fileNameLength > 0) {
this.filename = bstream.readString(this.fileNameLength);
}
this.extraField = null;
if (this.extraFieldLength > 0) {
this.extraField = bstream.readString(this.extraFieldLength);
info(" extra field=" + this.extraField);
}
// read in the compressed data
this.fileData = null;
if (this.compressedSize > 0) {
this.fileData = new Uint8Array(bstream.bytes.buffer, bstream.ptr, this.compressedSize);
bstream.ptr += this.compressedSize;
}
// TODO: deal with data descriptor if present (we currently assume no data descriptor!)
// "This descriptor exists only if bit 3 of the general purpose bit flag is set"
// But how do you figure out how big the file data is if you don't know the compressedSize
// from the header?!?
if ((this.generalPurpose & bitjs.BIT[3]) !== 0) {
this.crc32 = bstream.readNumber(4);
this.compressedSize = bstream.readNumber(4);
this.uncompressedSize = bstream.readNumber(4);
}
// Now that we have all the bytes for this file, we can print out some information.
info("Zip Local File Header:");
info(" version=" + this.version);
info(" general purpose=" + this.generalPurpose);
info(" compression method=" + this.compressionMethod);
info(" last mod file time=" + this.lastModFileTime);
info(" last mod file date=" + this.lastModFileDate);
info(" crc32=" + this.crc32);
info(" compressed size=" + this.compressedSize);
info(" uncompressed size=" + this.uncompressedSize);
info(" file name length=" + this.fileNameLength);
info(" extra field length=" + this.extraFieldLength);
info(" filename = '" + this.filename + "'");
};
// determine what kind of compressed data we have and decompress
ZipLocalFile.prototype.unzip = function() {
// Zip Version 1.0, no compression (store only)
if (this.compressionMethod === 0 ) {
info("ZIP v" + this.version + ", store only: " + this.filename + " (" + this.compressedSize + " bytes)");
currentBytesUnarchivedInFile = this.compressedSize;
currentBytesUnarchived += this.compressedSize;
this.fileData = zeroCompression(this.fileData, this.uncompressedSize);
} else if (this.compressionMethod === 8) {
// version == 20, compression method == 8 (DEFLATE)
info("ZIP v2.0, DEFLATE: " + this.filename + " (" + this.compressedSize + " bytes)");
this.fileData = inflate(this.fileData, this.uncompressedSize);
} else {
err("UNSUPPORTED VERSION/FORMAT: ZIP v" + this.version + ", compression method=" + this.compressionMethod + ": " + this.filename + " (" + this.compressedSize + " bytes)");
this.fileData = null;
}
};
// Takes an ArrayBuffer of a zip file in
// returns null on error
// returns an array of DecompressedFile objects on success
// ToDo This function differs
var unzip = function(arrayBuffer) {
postMessage(new bitjs.archive.UnarchiveStartEvent());
currentFilename = "";
currentFileNumber = 0;
currentBytesUnarchivedInFile = 0;
currentBytesUnarchived = 0;
totalUncompressedBytesInArchive = 0;
totalFilesInArchive = 0;
currentBytesUnarchived = 0;
var bstream = new bitjs.io.ByteStream(arrayBuffer);
// detect local file header signature or return null
if (bstream.peekNumber(4) === zLocalFileHeaderSignature) {
var localFiles = [];
// loop until we don't see any more local files
while (bstream.peekNumber(4) === zLocalFileHeaderSignature) {
var oneLocalFile = new ZipLocalFile(bstream);
// this should strip out directories/folders
if (oneLocalFile && oneLocalFile.uncompressedSize > 0 && oneLocalFile.fileData) {
localFiles.push(oneLocalFile);
totalUncompressedBytesInArchive += oneLocalFile.uncompressedSize;
}
}
totalFilesInArchive = localFiles.length;
// got all local files, now sort them
localFiles.sort(alphanumCase);
// archive extra data record
if (bstream.peekNumber(4) === zArchiveExtraDataSignature) {
info(" Found an Archive Extra Data Signature");
// skipping this record for now
bstream.readNumber(4);
var archiveExtraFieldLength = bstream.readNumber(4);
bstream.readString(archiveExtraFieldLength);
}
// central directory structure
// TODO: handle the rest of the structures (Zip64 stuff)
if (bstream.peekNumber(4) === zCentralFileHeaderSignature) {
info(" Found a Central File Header");
// read all file headers
while (bstream.peekNumber(4) === zCentralFileHeaderSignature) {
bstream.readNumber(4); // signature
bstream.readNumber(2); // version made by
bstream.readNumber(2); // version needed to extract
bstream.readNumber(2); // general purpose bit flag
bstream.readNumber(2); // compression method
bstream.readNumber(2); // last mod file time
bstream.readNumber(2); // last mod file date
bstream.readNumber(4); // crc32
bstream.readNumber(4); // compressed size
bstream.readNumber(4); // uncompressed size
var fileNameLength = bstream.readNumber(2); // file name length
var extraFieldLength = bstream.readNumber(2); // extra field length
var fileCommentLength = bstream.readNumber(2); // file comment length
bstream.readNumber(2); // disk number start
bstream.readNumber(2); // internal file attributes
bstream.readNumber(4); // external file attributes
bstream.readNumber(4); // relative offset of local header
bstream.readString(fileNameLength); // file name
bstream.readString(extraFieldLength); // extra field
bstream.readString(fileCommentLength); // file comment
}
}
// digital signature
if (bstream.peekNumber(4) === zDigitalSignatureSignature) {
info(" Found a Digital Signature");
bstream.readNumber(4);
var sizeOfSignature = bstream.readNumber(2);
bstream.readString(sizeOfSignature); // digital signature data
}
// report # files and total length
if (localFiles.length > 0) {
postProgress();
}
// now do the unzipping of each file
for (var i = 0; i < localFiles.length; ++i) {
var localfile = localFiles[i];
// update progress
currentFilename = localfile.filename;
currentFileNumber = i;
currentBytesUnarchivedInFile = 0;
// actually do the unzipping
localfile.unzip();
if (localfile.fileData !== null) {
postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile));
postProgress();
}
}
postProgress();
postMessage(new bitjs.archive.UnarchiveFinishEvent());
}
};
// returns a table of Huffman codes
// each entry's index is its code and its value is a JavaScript object
// containing {length: 6, symbol: X}
function getHuffmanCodes(bitLengths) {
// ensure bitLengths is an array containing at least one element
if (typeof bitLengths !== typeof [] || bitLengths.length < 1) {
err("Error! getHuffmanCodes() called with an invalid array");
return null;
}
// Reference: http://tools.ietf.org/html/rfc1951#page-8
var numLengths = bitLengths.length;
var blCount = [];
var MAX_BITS = 1;
// Step 1: count up how many codes of each length we have
for (var i = 0; i < numLengths; ++i) {
var length = bitLengths[i];
// test to ensure each bit length is a positive, non-zero number
if (typeof length !== typeof 1 || length < 0) {
err("bitLengths contained an invalid number in getHuffmanCodes(): " + length + " of type " + (typeof length));
return null;
}
// increment the appropriate bitlength count
if (typeof blCount[length] === "undefined") blCount[length] = 0;
// a length of zero means this symbol is not participating in the huffman coding
if (length > 0) blCount[length]++;
if (length > MAX_BITS) MAX_BITS = length;
}
// Step 2: Find the numerical value of the smallest code for each code length
var nextCode = [];
var code = 0;
for (var bits = 1; bits <= MAX_BITS; ++bits) {
var length2 = bits - 1;
// ensure undefined lengths are zero
if (typeof blCount[length2] === "undefined") blCount[length2] = 0;
code = (code + blCount[bits - 1]) << 1;
nextCode [bits] = code;
}
// Step 3: Assign numerical values to all codes
var table = {};
var tableLength = 0;
for (var n = 0; n < numLengths; ++n) {
var len = bitLengths[n];
if (len !== 0) {
table[nextCode [len]] = { length: len, symbol: n }; //, bitstring: binaryValueToString(nextCode [len],len) };
tableLength++;
nextCode [len]++;
}
}
table.maxLength = tableLength;
return table;
}
/*
The Huffman codes for the two alphabets are fixed, and are not
represented explicitly in the data. The Huffman code lengths
for the literal/length alphabet are:
Lit Value Bits Codes
--------- ---- -----
0 - 143 8 00110000 through
10111111
144 - 255 9 110010000 through
111111111
256 - 279 7 0000000 through
0010111
280 - 287 8 11000000 through
11000111
*/
// fixed Huffman codes go from 7-9 bits, so we need an array whose index can hold up to 9 bits
var fixedHCtoLiteral = null;
var fixedHCtoDistance = null;
function getFixedLiteralTable() {
// create once
if (!fixedHCtoLiteral) {
var bitlengths = new Array(288);
var i;
for (i = 0; i <= 143; ++i) bitlengths[i] = 8;
for (i = 144; i <= 255; ++i) bitlengths[i] = 9;
for (i = 256; i <= 279; ++i) bitlengths[i] = 7;
for (i = 280; i <= 287; ++i) bitlengths[i] = 8;
// get huffman code table
fixedHCtoLiteral = getHuffmanCodes(bitlengths);
}
return fixedHCtoLiteral;
}
function getFixedDistanceTable() {
// create once
if (!fixedHCtoDistance) {
var bitlengths = new Array(32);
for (var i = 0; i < 32; ++i) {
bitlengths[i] = 5;
}
// get huffman code table
fixedHCtoDistance = getHuffmanCodes(bitlengths);
}
return fixedHCtoDistance;
}
// extract one bit at a time until we find a matching Huffman Code
// then return that symbol
function decodeSymbol(bstream, hcTable) {
var code = 0;
var len = 0;
// loop until we match
for (;;) {
// read in next bit
var bit = bstream.readBits(1);
code = (code << 1) | bit;
++len;
// check against Huffman Code table and break if found
if (hcTable.hasOwnProperty(code) && hcTable[code].length === len) {
break;
}
if (len > hcTable.maxLength) {
err("Bit stream out of sync, didn't find a Huffman Code, length was " + len +
" and table only max code length of " + hcTable.maxLength);
break;
}
}
return hcTable[code].symbol;
}
var CodeLengthCodeOrder = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15];
/*
Extra Extra Extra
Code Bits Length(s) Code Bits Lengths Code Bits Length(s)
---- ---- ------ ---- ---- ------- ---- ---- -------
257 0 3 267 1 15,16 277 4 67-82
258 0 4 268 1 17,18 278 4 83-98
259 0 5 269 2 19-22 279 4 99-114
260 0 6 270 2 23-26 280 4 115-130
261 0 7 271 2 27-30 281 5 131-162
262 0 8 272 2 31-34 282 5 163-194
263 0 9 273 3 35-42 283 5 195-226
264 0 10 274 3 43-50 284 5 227-257
265 1 11,12 275 3 51-58 285 0 258
266 1 13,14 276 3 59-66
*/
var LengthLookupTable = [
[0, 3],
[0, 4],
[0, 5],
[0, 6],
[0, 7],
[0, 8],
[0, 9],
[0, 10],
[1, 11],
[1, 13],
[1, 15],
[1, 17],
[2, 19],
[2, 23],
[2, 27],
[2, 31],
[3, 35],
[3, 43],
[3, 51],
[3, 59],
[4, 67],
[4, 83],
[4, 99],
[4, 115],
[5, 131],
[5, 163],
[5, 195],
[5, 227],
[0, 258]
];
/*
Extra Extra Extra
Code Bits Dist Code Bits Dist Code Bits Distance
---- ---- ---- ---- ---- ------ ---- ---- --------
0 0 1 10 4 33-48 20 9 1025-1536
1 0 2 11 4 49-64 21 9 1537-2048
2 0 3 12 5 65-96 22 10 2049-3072
3 0 4 13 5 97-128 23 10 3073-4096
4 1 5,6 14 6 129-192 24 11 4097-6144
5 1 7,8 15 6 193-256 25 11 6145-8192
6 2 9-12 16 7 257-384 26 12 8193-12288
7 2 13-16 17 7 385-512 27 12 12289-16384
8 3 17-24 18 8 513-768 28 13 16385-24576
9 3 25-32 19 8 769-1024 29 13 24577-32768
*/
var DistLookupTable = [
[0, 1],
[0, 2],
[0, 3],
[0, 4],
[1, 5],
[1, 7],
[2, 9],
[2, 13],
[3, 17],
[3, 25],
[4, 33],
[4, 49],
[5, 65],
[5, 97],
[6, 129],
[6, 193],
[7, 257],
[7, 385],
[8, 513],
[8, 769],
[9, 1025],
[9, 1537],
[10, 2049],
[10, 3073],
[11, 4097],
[11, 6145],
[12, 8193],
[12, 12289],
[13, 16385],
[13, 24577]
];
function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) {
/*
loop (until end of block code recognized)
decode literal/length value from input stream
if value < 256
copy value (literal byte) to output stream
otherwise
if value = end of block (256)
break from loop
otherwise (value = 257..285)
decode distance from input stream
move backwards distance bytes in the output
stream, and copy length bytes from this
position to the output stream.
*/
var blockSize = 0;
for (;;) {
var symbol = decodeSymbol(bstream, hcLiteralTable);
if (symbol < 256) {
// copy literal byte to output
buffer.insertByte(symbol);
blockSize++;
} else {
// end of block reached
if (symbol === 256) {
break;
} else {
var lengthLookup = LengthLookupTable[symbol - 257];
var length = lengthLookup[1] + bstream.readBits(lengthLookup[0]);
var distLookup = DistLookupTable[decodeSymbol(bstream, hcDistanceTable)];
var distance = distLookup[1] + bstream.readBits(distLookup[0]);
// now apply length and distance appropriately and copy to output
// TODO: check that backward distance < data.length?
// http://tools.ietf.org/html/rfc1951#page-11
// "Note also that the referenced string may overlap the current
// position; for example, if the last 2 bytes decoded have values
// X and Y, a string reference with <length = 5, distance = 2>
// adds X,Y,X,Y,X to the output stream."
//
// loop for each character
var ch = buffer.ptr - distance;
blockSize += length;
if (length > distance) {
var data = buffer.data;
while (length--) {
buffer.insertByte(data[ch++]);
}
} else {
buffer.insertBytes(buffer.data.subarray(ch, ch + length));
}
} // length-distance pair
} // length-distance pair or end-of-block
} // loop until we reach end of block
return blockSize;
}
function zeroCompression(compressedData, numDecompressedBytes) {
var bstream = new bitjs.io.BitStream(compressedData.buffer,
false /* rtl */,
compressedData.byteOffset,
compressedData.byteLength);
var buffer = new bitjs.io.ByteBuffer(numDecompressedBytes);
buffer.insertBytes(bstream.readBytes(numDecompressedBytes));
return buffer.data;
}
// {Uint8Array} compressedData A Uint8Array of the compressed file data.
// compression method 8
// deflate: http://tools.ietf.org/html/rfc1951
function inflate(compressedData, numDecompressedBytes) {
// Bit stream representing the compressed data.
var bstream = new bitjs.io.BitStream(compressedData.buffer,
false /* rtl */,
compressedData.byteOffset,
compressedData.byteLength);
var buffer = new bitjs.io.ByteBuffer(numDecompressedBytes);
var blockSize = 0;
// block format: http://tools.ietf.org/html/rfc1951#page-9
var bFinal = 0;
do {
bFinal = bstream.readBits(1);
var bType = bstream.readBits(2);
blockSize = 0;
// ++numBlocks;
// no compression
if (bType === 0) {
// skip remaining bits in this byte
while (bstream.bitPtr !== 0) bstream.readBits(1);
var len = bstream.readBits(16);
bstream.readBits(16);
// TODO: check if nlen is the ones-complement of len?
if (len > 0) buffer.insertBytes(bstream.readBytes(len));
blockSize = len;
} else if (bType === 1) {
// fixed Huffman codes
blockSize = inflateBlockData(bstream, getFixedLiteralTable(), getFixedDistanceTable(), buffer);
} else if (bType === 2) {
// dynamic Huffman codes
var numLiteralLengthCodes = bstream.readBits(5) + 257;
var numDistanceCodes = bstream.readBits(5) + 1,
numCodeLengthCodes = bstream.readBits(4) + 4;
// populate the array of code length codes (first de-compaction)
var codeLengthsCodeLengths = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for (var i = 0; i < numCodeLengthCodes; ++i) {
codeLengthsCodeLengths[ CodeLengthCodeOrder[i] ] = bstream.readBits(3);
}
// get the Huffman Codes for the code lengths
var codeLengthsCodes = getHuffmanCodes(codeLengthsCodeLengths);
// now follow this mapping
/*
0 - 15: Represent code lengths of 0 - 15
16: Copy the previous code length 3 - 6 times.
The next 2 bits indicate repeat length
(0 = 3, ... , 3 = 6)
Example: Codes 8, 16 (+2 bits 11),
16 (+2 bits 10) will expand to
12 code lengths of 8 (1 + 6 + 5)
17: Repeat a code length of 0 for 3 - 10 times.
(3 bits of length)
18: Repeat a code length of 0 for 11 - 138 times
(7 bits of length)
*/
// to generate the true code lengths of the Huffman Codes for the literal
// and distance tables together
var literalCodeLengths = [];
var prevCodeLength = 0;
while (literalCodeLengths.length < numLiteralLengthCodes + numDistanceCodes) {
var symbol = decodeSymbol(bstream, codeLengthsCodes);
if (symbol <= 15) {
literalCodeLengths.push(symbol);
prevCodeLength = symbol;
} else if (symbol === 16) {
var repeat = bstream.readBits(2) + 3;
while (repeat--) {
literalCodeLengths.push(prevCodeLength);
}
} else if (symbol === 17) {
var repeat1 = bstream.readBits(3) + 3;
while (repeat1--) {
literalCodeLengths.push(0);
}
} else if (symbol === 18) {
var repeat2 = bstream.readBits(7) + 11;
while (repeat2--) {
literalCodeLengths.push(0);
}
}
}
// now split the distance code lengths out of the literal code array
var distanceCodeLengths = literalCodeLengths.splice(numLiteralLengthCodes, numDistanceCodes);
// now generate the true Huffman Code tables using these code lengths
var hcLiteralTable = getHuffmanCodes(literalCodeLengths);
var hcDistanceTable = getHuffmanCodes(distanceCodeLengths);
blockSize = inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer);
} else {
// error
err("Error! Encountered deflate block of type 3");
return null;
}
// update progress
currentBytesUnarchivedInFile += blockSize;
currentBytesUnarchived += blockSize;
postProgress();
} while (bFinal !== 1);
// we are done reading blocks if the bFinal bit was set for this block
// return the buffer data bytes
return buffer.data;
}
// event.data.file has the ArrayBuffer.
onmessage = function(event) {
unzip(event.data.file, true);
};

View File

@ -412,7 +412,7 @@ if($("body.advsearch").length > 0) {
$("#add-to-shelves").toggle();
});
$('#add-to-shelf').height("40px");
function dropdownToggle() {
function search_dropdownToggle() {
topPos = $("#add-to-shelf").offset().top-20;
if ($('div[aria-label="Add to shelves"]').length > 0) {
@ -428,10 +428,10 @@ if($("body.advsearch").length > 0) {
}
}
dropdownToggle();
search_dropdownToggle();
$(window).on("resize", function () {
dropdownToggle();
search_dropdownToggle();
});
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,90 @@
// Copyright (c) 2017 Matthew Brennan Jones <matthew.brennan.jones@gmail.com>
// This software is licensed under a MIT License
// https://github.com/workhorsy/uncompress.js
"use strict";
// Based on the information from:
// https://en.wikipedia.org/wiki/Tar_(computing)
(function() {
const TAR_TYPE_FILE = 0;
const TAR_TYPE_DIR = 5;
const TAR_HEADER_SIZE = 512;
const TAR_TYPE_OFFSET = 156;
const TAR_TYPE_SIZE = 1;
const TAR_SIZE_OFFSET = 124;
const TAR_SIZE_SIZE = 12;
const TAR_NAME_OFFSET = 0;
const TAR_NAME_SIZE = 100;
function _tarRead(view, offset, size) {
return view.slice(offset, offset + size);
}
function tarGetEntries(filename, array_buffer) {
let view = new Uint8Array(array_buffer);
let offset = 0;
let entries = [];
while (offset + TAR_HEADER_SIZE < view.byteLength) {
// Get entry name
let entry_name = saneMap(_tarRead(view, offset + TAR_NAME_OFFSET, TAR_NAME_SIZE), String.fromCharCode);
entry_name = entry_name.join('').replace(/\0/g, '');
// No entry name, so probably the last block
if (entry_name.length === 0) {
break;
}
// Get entry size
let entry_size = parseInt(saneJoin(saneMap(_tarRead(view, offset + TAR_SIZE_OFFSET, TAR_SIZE_SIZE), String.fromCharCode), ''), 8);
let entry_type = saneMap(_tarRead(view, offset + TAR_TYPE_OFFSET, TAR_TYPE_SIZE), String.fromCharCode) | 0;
// Save this as en entry if it is a file or directory
if (entry_type === TAR_TYPE_FILE || entry_type === TAR_TYPE_DIR) {
let entry = {
name: entry_name,
size: entry_size,
is_file: entry_type == TAR_TYPE_FILE,
offset: offset
};
entries.push(entry);
}
// Round the offset up to be divisible by TAR_HEADER_SIZE
offset += (entry_size + TAR_HEADER_SIZE);
if (offset % TAR_HEADER_SIZE > 0) {
let even = (offset / TAR_HEADER_SIZE) | 0; // number of times it goes evenly into TAR_HEADER_SIZE
offset = (even + 1) * TAR_HEADER_SIZE;
}
}
return entries;
}
function tarGetEntryData(entry, array_buffer) {
let view = new Uint8Array(array_buffer);
let offset = entry.offset;
let size = entry.size;
// Get entry data
let entry_data = _tarRead(view, offset + TAR_HEADER_SIZE, size);
return entry_data;
}
// Figure out if we are running in a Window or Web Worker
let scope = null;
if (typeof window === 'object') {
scope = window;
} else if (typeof importScripts === 'function') {
scope = self;
}
// Set exports
scope.tarGetEntries = tarGetEntries;
scope.tarGetEntryData = tarGetEntryData;
})();

View File

@ -0,0 +1,420 @@
// Copyright (c) 2017 Matthew Brennan Jones <matthew.brennan.jones@gmail.com>
// This software is licensed under a MIT License
// https://github.com/workhorsy/uncompress.js
"use strict";
function loadScript(url, cb) {
// Window
if (typeof window === 'object') {
let script = document.createElement('script');
script.type = "text/javascript";
script.src = url;
script.onload = function() {
if (cb) cb();
};
document.head.appendChild(script);
// Web Worker
} else if (typeof importScripts === 'function') {
importScripts(url);
if (cb) cb();
}
}
function currentScriptPath() {
// NOTE: document.currentScript does not work in a Web Worker
// So we have to parse a stack trace maually
try {
throw new Error('');
} catch(e) {
let stack = e.stack;
let line = null;
// Chrome and IE
if (stack.indexOf('@') !== -1) {
line = stack.split('@')[1].split('\n')[0];
// Firefox
} else {
line = stack.split('(')[1].split(')')[0];
}
line = line.substring(0, line.lastIndexOf('/')) + '/';
return line;
}
}
// This is used by libunrar.js to load libunrar.js.mem
let unrarMemoryFileLocation = null;
let g_on_loaded_cb = null;
(function() {
let _loaded_archive_formats = [];
// Polyfill for missing array slice method (IE 11)
if (typeof Uint8Array !== 'undefined') {
if (! Uint8Array.prototype.slice) {
Uint8Array.prototype.slice = function(start, end) {
let retval = new Uint8Array(end - start);
let j = 0;
for (let i=start; i<end; ++i) {
retval[j] = this[i];
j++;
}
return retval;
};
}
}
// FIXME: This function is super inefficient
function saneJoin(array, separator) {
let retval = '';
for (let i=0; i<array.length; ++i) {
if (i === 0) {
retval += array[i];
} else {
retval += separator + array[i];
}
}
return retval;
}
function saneMap(array, cb) {
let retval = new Array(array.length);
for (let i=0; i<retval.length; ++i) {
retval[i] = cb(array[i]);
}
return retval;
}
function loadArchiveFormats(formats, cb) {
// Get the path of the current script
let path = currentScriptPath();
let load_counter = 0;
let checkForLoadDone = function() {
load_counter++;
// Get the total number of loads before we are done loading
// If loading RAR in a Window, have 1 extra load.
let load_total = formats.length;
if (formats.indexOf('rar') !== -1 && typeof window === 'object') {
load_total++;
}
// run the callback if the last script has loaded
if (load_counter === load_total) {
cb();
}
};
g_on_loaded_cb = checkForLoadDone;
// Load the formats
formats.forEach(function(archive_format) {
// Skip this format if it is already loaded
if (_loaded_archive_formats.indexOf(archive_format) !== -1) {
return;
}
// Load the archive format
switch (archive_format) {
case 'rar':
unrarMemoryFileLocation = path + 'libunrar.js.mem';
loadScript(path + 'libunrar.js', checkForLoadDone);
_loaded_archive_formats.push(archive_format);
break;
case 'zip':
loadScript(path + 'jszip.js', checkForLoadDone);
_loaded_archive_formats.push(archive_format);
break;
case 'tar':
loadScript(path + 'libuntar.js', checkForLoadDone);
_loaded_archive_formats.push(archive_format);
break;
default:
throw new Error("Unknown archive format '" + archive_format + "'.");
}
});
}
function archiveOpenFile(array_buffer, cb) {
let file_name = "Hugo"; //file.name;
let password = null;
try {
let archive = archiveOpenArrayBuffer(file_name, password, array_buffer);
cb(archive, null);
} catch(e) {
cb(null, e);
}
}
function archiveOpenArrayBuffer(file_name, password, array_buffer) {
// Get the archive type
let archive_type = null;
if (isRarFile(array_buffer)) {
archive_type = 'rar';
} else if(isZipFile(array_buffer)) {
archive_type = 'zip';
} else if(isTarFile(array_buffer)) {
archive_type = 'tar';
} else {
throw new Error("The archive type is unknown");
}
// Make sure the archive format is loaded
if (_loaded_archive_formats.indexOf(archive_type) === -1) {
throw new Error("The archive format '" + archive_type + "' is not loaded.");
}
// Get the entries
let handle = null;
let entries = [];
try {
switch (archive_type) {
case 'rar':
handle = _rarOpen(file_name, password, array_buffer);
entries = _rarGetEntries(handle);
break;
case 'zip':
handle = _zipOpen(file_name, password, array_buffer);
entries = _zipGetEntries(handle);
break;
case 'tar':
handle = _tarOpen(file_name, password, array_buffer);
entries = _tarGetEntries(handle);
break;
}
} catch(e) {
throw new Error("Failed to open '" + archive_type + "' archive.");
}
// Sort the entries by name
entries.sort(function(a, b) {
if(a.name < b.name) return -1;
if(a.name > b.name) return 1;
return 0;
});
// Return the archive object
return {
file_name: file_name,
archive_type: archive_type,
array_buffer: array_buffer,
entries: entries,
handle: handle
};
}
function archiveClose(archive) {
archive.file_name = null;
archive.archive_type = null;
archive.array_buffer = null;
archive.entries = null;
archive.handle = null;
}
function _rarOpen(file_name, password, array_buffer) {
// Create an array of rar files
let rar_files = [{
name: file_name,
size: array_buffer.byteLength,
type: '',
content: new Uint8Array(array_buffer)
}];
// Return rar handle
return {
file_name: file_name,
array_buffer: array_buffer,
password: password,
rar_files: rar_files
};
}
function _zipOpen(file_name, password, array_buffer) {
let zip = new JSZip(array_buffer);
// Return zip handle
return {
file_name: file_name,
array_buffer: array_buffer,
password: password,
zip: zip
};
}
function _tarOpen(file_name, password, array_buffer) {
// Return tar handle
return {
file_name: file_name,
array_buffer: array_buffer,
password: password
};
}
function _rarGetEntries(rar_handle) {
// Get the entries
let info = readRARFileNames(rar_handle.rar_files, rar_handle.password);
let entries = [];
Object.keys(info).forEach(function(i) {
let name = info[i].name;
let is_file = info[i].is_file;
if (is_file) {
entries.push({
name: name,
is_file: is_file, // info[i].is_file,
size_compressed: info[i].size_compressed,
size_uncompressed: info[i].size_uncompressed,
readData: function (cb) {
setTimeout(function () {
if (is_file) {
try {
readRARContent(rar_handle.rar_files, rar_handle.password, name, cb);
} catch (e) {
cb(null, e);
}
} else {
cb(null, null);
}
}, 0);
}
});
}
});
return entries;
}
function _zipGetEntries(zip_handle) {
let zip = zip_handle.zip;
// Get all the entries
let entries = [];
Object.keys(zip.files).forEach(function(i) {
let zip_entry = zip.files[i];
let name = zip_entry.name;
let is_file = ! zip_entry.dir;
let size_compressed = zip_entry._data ? zip_entry._data.compressedSize : 0;
let size_uncompressed = zip_entry._data ? zip_entry._data.uncompressedSize : 0;
if (is_file) {
entries.push({
name: name,
is_file: is_file,
size_compressed: size_compressed,
size_uncompressed: size_uncompressed,
readData: function (cb) {
setTimeout(function () {
if (is_file) {
let data = zip_entry.asArrayBuffer();
cb(data, null);
} else {
cb(null, null);
}
}, 0);
}
});
}
});
return entries;
}
function _tarGetEntries(tar_handle) {
let tar_entries = tarGetEntries(tar_handle.file_name, tar_handle.array_buffer);
// Get all the entries
let entries = [];
tar_entries.forEach(function(entry) {
let name = entry.name;
let is_file = entry.is_file;
let size = entry.size;
if (is_file) {
entries.push({
name: name,
is_file: is_file,
size_compressed: size,
size_uncompressed: size,
readData: function (cb) {
setTimeout(function () {
if (is_file) {
let data = tarGetEntryData(entry, tar_handle.array_buffer);
cb(data.buffer, null);
} else {
cb(null, null);
}
}, 0);
}
});
}
});
return entries;
}
function isRarFile(array_buffer) {
// The three styles of RAR headers
let rar_header1 = saneJoin([0x52, 0x45, 0x7E, 0x5E], ', '); // old
let rar_header2 = saneJoin([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00], ', '); // 1.5 to 4.0
let rar_header3 = saneJoin([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00], ', '); // 5.0
// Just return false if the file is smaller than the header
if (array_buffer.byteLength < 8) {
return false;
}
// Return true if the header matches one of the RAR headers
let header1 = saneJoin(new Uint8Array(array_buffer).slice(0, 4), ', ');
let header2 = saneJoin(new Uint8Array(array_buffer).slice(0, 7), ', ');
let header3 = saneJoin(new Uint8Array(array_buffer).slice(0, 8), ', ');
return (header1 === rar_header1 || header2 === rar_header2 || header3 === rar_header3);
}
function isZipFile(array_buffer) {
// The ZIP header
let zip_header = saneJoin([0x50, 0x4b, 0x03, 0x04], ', ');
// Just return false if the file is smaller than the header
if (array_buffer.byteLength < 4) {
return false;
}
// Return true if the header matches the ZIP header
let header = saneJoin(new Uint8Array(array_buffer).slice(0, 4), ', ');
return (header === zip_header);
}
function isTarFile(array_buffer) {
// The TAR header
let tar_header = saneJoin(['u', 's', 't', 'a', 'r'], ', ');
// Just return false if the file is smaller than the header size
if (array_buffer.byteLength < 512) {
return false;
}
// Return true if the header matches the TAR header
let header = saneJoin(saneMap(new Uint8Array(array_buffer).slice(257, 257 + 5), String.fromCharCode), ', ');
return (header === tar_header);
}
// Figure out if we are running in a Window or Web Worker
let scope = null;
if (typeof window === 'object') {
scope = window;
} else if (typeof importScripts === 'function') {
scope = self;
}
// Set exports
scope.loadArchiveFormats = loadArchiveFormats;
scope.archiveOpenFile = archiveOpenFile;
scope.archiveOpenArrayBuffer = archiveOpenArrayBuffer;
scope.archiveClose = archiveClose;
scope.isRarFile = isRarFile;
scope.isZipFile = isZipFile;
scope.isTarFile = isTarFile;
scope.saneJoin = saneJoin;
scope.saneMap = saneMap;
})();

View File

@ -263,3 +263,9 @@ $("#btn-upload-cover").on("change", function () {
$("#upload-cover").html(filename);
});
$("#xchange").click(function () {
this.blur();
var title = $("#book_title").val();
$("#book_title").val($("#bookAuthor").val());
$("#bookAuthor").val(title);
});

View File

@ -0,0 +1,45 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2021 OzzieIsaacs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
function toggleFullscreen(elem) {
if (!document.fullscreenElement && !document.mozFullScreenElement &&
!document.webkitFullscreenElement && !document.msFullscreenElement) {
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem.msRequestFullscreen) {
elem.msRequestFullscreen();
} else if (elem.mozRequestFullScreen) {
elem.mozRequestFullScreen();
} else if (elem.webkitRequestFullscreen) {
elem.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
}
$("#detailcover").click(function() {
toggleFullscreen(this);
});

View File

@ -1,237 +0,0 @@
/*
* bitstream.js
*
* Provides readers for bitstreams.
*
* Licensed under the MIT License
*
* Copyright(c) 2011 Google Inc.
* Copyright(c) 2011 antimatter15
*/
/* global bitjs, Uint8Array */
var bitjs = bitjs || {};
bitjs.io = bitjs.io || {};
(function() {
// mask for getting the Nth bit (zero-based)
bitjs.BIT = [0x01, 0x02, 0x04, 0x08,
0x10, 0x20, 0x40, 0x80,
0x100, 0x200, 0x400, 0x800,
0x1000, 0x2000, 0x4000, 0x8000
];
// mask for getting N number of bits (0-8)
var BITMASK = [0, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF];
/**
* This bit stream peeks and consumes bits out of a binary stream.
*
* @param {ArrayBuffer} ab An ArrayBuffer object or a Uint8Array.
* @param {boolean} rtl Whether the stream reads bits from the byte starting
* from bit 7 to 0 (true) or bit 0 to 7 (false).
* @param {Number} optOffset The offset into the ArrayBuffer
* @param {Number} optLength The length of this BitStream
*/
bitjs.io.BitStream = function(ab, rtl, optOffset, optLength) {
if (!ab || !ab.toString || ab.toString() !== "[object ArrayBuffer]") {
throw "Error! BitArray constructed with an invalid ArrayBuffer object";
}
var offset = optOffset || 0;
var length = optLength || ab.byteLength;
this.bytes = new Uint8Array(ab, offset, length);
this.bytePtr = 0; // tracks which byte we are on
this.bitPtr = 0; // tracks which bit we are on (can have values 0 through 7)
this.peekBits = rtl ? this.peekBitsRtl : this.peekBitsLtr;
};
/**
* byte0 byte1 byte2 byte3
* 7......0 | 7......0 | 7......0 | 7......0
*
* The bit pointer starts at bit0 of byte0 and moves left until it reaches
* bit7 of byte0, then jumps to bit0 of byte1, etc.
* @param {number} n The number of bits to peek.
* @param {boolean=} movePointers Whether to move the pointer, defaults false.
* @return {number} The peeked bits, as an unsigned number.
*/
bitjs.io.BitStream.prototype.peekBitsLtr = function(n, movePointers) {
if (n <= 0 || typeof n !== typeof 1) {
return 0;
}
var movePointers = movePointers || false,
bytePtr = this.bytePtr,
bitPtr = this.bitPtr,
result = 0,
bitsIn = 0,
bytes = this.bytes;
// keep going until we have no more bits left to peek at
// TODO: Consider putting all bits from bytes we will need into a variable and then
// shifting/masking it to just extract the bits we want.
// This could be considerably faster when reading more than 3 or 4 bits at a time.
while (n > 0) {
if (bytePtr >= bytes.length) {
throw "Error! Overflowed the bit stream! n=" + n + ", bytePtr=" + bytePtr + ", bytes.length=" +
bytes.length + ", bitPtr=" + bitPtr;
// return -1;
}
var numBitsLeftInThisByte = (8 - bitPtr);
var mask;
if (n >= numBitsLeftInThisByte) {
mask = (BITMASK[numBitsLeftInThisByte] << bitPtr);
result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn);
bytePtr++;
bitPtr = 0;
bitsIn += numBitsLeftInThisByte;
n -= numBitsLeftInThisByte;
} else {
mask = (BITMASK[n] << bitPtr);
result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn);
bitPtr += n;
bitsIn += n;
n = 0;
}
}
if (movePointers) {
this.bitPtr = bitPtr;
this.bytePtr = bytePtr;
}
return result;
};
/**
* byte0 byte1 byte2 byte3
* 7......0 | 7......0 | 7......0 | 7......0
*
* The bit pointer starts at bit7 of byte0 and moves right until it reaches
* bit0 of byte0, then goes to bit7 of byte1, etc.
* @param {number} n The number of bits to peek.
* @param {boolean=} movePointers Whether to move the pointer, defaults false.
* @return {number} The peeked bits, as an unsigned number.
*/
bitjs.io.BitStream.prototype.peekBitsRtl = function(n, movePointers) {
if (n <= 0 || typeof n !== typeof 1) {
return 0;
}
var movePointers = movePointers || false,
bytePtr = this.bytePtr,
bitPtr = this.bitPtr,
result = 0,
bytes = this.bytes;
// keep going until we have no more bits left to peek at
// TODO: Consider putting all bits from bytes we will need into a variable and then
// shifting/masking it to just extract the bits we want.
// This could be considerably faster when reading more than 3 or 4 bits at a time.
while (n > 0) {
if (bytePtr >= bytes.length) {
throw "Error! Overflowed the bit stream! n=" + n + ", bytePtr=" + bytePtr + ", bytes.length=" +
bytes.length + ", bitPtr=" + bitPtr;
// return -1;
}
var numBitsLeftInThisByte = (8 - bitPtr);
if (n >= numBitsLeftInThisByte) {
result <<= numBitsLeftInThisByte;
result |= (BITMASK[numBitsLeftInThisByte] & bytes[bytePtr]);
bytePtr++;
bitPtr = 0;
n -= numBitsLeftInThisByte;
} else {
result <<= n;
result |= ((bytes[bytePtr] & (BITMASK[n] << (8 - n - bitPtr))) >> (8 - n - bitPtr));
bitPtr += n;
n = 0;
}
}
if (movePointers) {
this.bitPtr = bitPtr;
this.bytePtr = bytePtr;
}
return result;
};
/**
* Peek at 16 bits from current position in the buffer.
* Bit at (bytePtr,bitPtr) has the highest position in returning data.
* Taken from getbits.hpp in unrar.
* TODO: Move this out of BitStream and into unrar.
*/
bitjs.io.BitStream.prototype.getBits = function() {
return (((((this.bytes[this.bytePtr] & 0xff) << 16) +
((this.bytes[this.bytePtr + 1] & 0xff) << 8) +
((this.bytes[this.bytePtr + 2] & 0xff))) >>> (8 - this.bitPtr)) & 0xffff);
};
/**
* Reads n bits out of the stream, consuming them (moving the bit pointer).
* @param {number} n The number of bits to read.
* @return {number} The read bits, as an unsigned number.
*/
bitjs.io.BitStream.prototype.readBits = function(n) {
return this.peekBits(n, true);
};
/**
* This returns n bytes as a sub-array, advancing the pointer if movePointers
* is true. Only use this for uncompressed blocks as this throws away remaining
* bits in the current byte.
* @param {number} n The number of bytes to peek.
* @param {boolean=} movePointers Whether to move the pointer, defaults false.
* @return {Uint8Array} The subarray.
*/
bitjs.io.BitStream.prototype.peekBytes = function(n, movePointers) {
if (n <= 0 || typeof n !== typeof 1) {
return 0;
}
// from http://tools.ietf.org/html/rfc1951#page-11
// "Any bits of input up to the next byte boundary are ignored."
while (this.bitPtr !== 0) {
this.readBits(1);
}
var movePointers = movePointers || false;
var bytePtr = this.bytePtr;
// bitPtr = this.bitPtr;
var result = this.bytes.subarray(bytePtr, bytePtr + n);
if (movePointers) {
this.bytePtr += n;
}
return result;
};
/**
* @param {number} n The number of bytes to read.
* @return {Uint8Array} The subarray.
*/
bitjs.io.BitStream.prototype.readBytes = function(n) {
return this.peekBytes(n, true);
};
})();

View File

@ -1,124 +0,0 @@
/*
* bytestream.js
*
* Provides a writer for bytes.
*
* Licensed under the MIT License
*
* Copyright(c) 2011 Google Inc.
* Copyright(c) 2011 antimatter15
*/
/* global bitjs, Uint8Array */
var bitjs = bitjs || {};
bitjs.io = bitjs.io || {};
(function() {
/**
* A write-only Byte buffer which uses a Uint8 Typed Array as a backing store.
* @param {number} numBytes The number of bytes to allocate.
* @constructor
*/
bitjs.io.ByteBuffer = function(numBytes) {
if (typeof numBytes !== typeof 1 || numBytes <= 0) {
throw "Error! ByteBuffer initialized with '" + numBytes + "'";
}
this.data = new Uint8Array(numBytes);
this.ptr = 0;
};
/**
* @param {number} b The byte to insert.
*/
bitjs.io.ByteBuffer.prototype.insertByte = function(b) {
// TODO: throw if byte is invalid?
this.data[this.ptr++] = b;
};
/**
* @param {Array.<number>|Uint8Array|Int8Array} bytes The bytes to insert.
*/
bitjs.io.ByteBuffer.prototype.insertBytes = function(bytes) {
// TODO: throw if bytes is invalid?
this.data.set(bytes, this.ptr);
this.ptr += bytes.length;
};
/**
* Writes an unsigned number into the next n bytes. If the number is too large
* to fit into n bytes or is negative, an error is thrown.
* @param {number} num The unsigned number to write.
* @param {number} numBytes The number of bytes to write the number into.
*/
bitjs.io.ByteBuffer.prototype.writeNumber = function(num, numBytes) {
if (numBytes < 1) {
throw "Trying to write into too few bytes: " + numBytes;
}
if (num < 0) {
throw "Trying to write a negative number (" + num +
") as an unsigned number to an ArrayBuffer";
}
if (num > (Math.pow(2, numBytes * 8) - 1)) {
throw "Trying to write " + num + " into only " + numBytes + " bytes";
}
// Roll 8-bits at a time into an array of bytes.
var bytes = [];
while (numBytes-- > 0) {
var eightBits = num & 255;
bytes.push(eightBits);
num >>= 8;
}
this.insertBytes(bytes);
};
/**
* Writes a signed number into the next n bytes. If the number is too large
* to fit into n bytes, an error is thrown.
* @param {number} num The signed number to write.
* @param {number} numBytes The number of bytes to write the number into.
*/
bitjs.io.ByteBuffer.prototype.writeSignedNumber = function(num, numBytes) {
if (numBytes < 1) {
throw "Trying to write into too few bytes: " + numBytes;
}
var HALF = Math.pow(2, (numBytes * 8) - 1);
if (num >= HALF || num < -HALF) {
throw "Trying to write " + num + " into only " + numBytes + " bytes";
}
// Roll 8-bits at a time into an array of bytes.
var bytes = [];
while (numBytes-- > 0) {
var eightBits = num & 255;
bytes.push(eightBits);
num >>= 8;
}
this.insertBytes(bytes);
};
/**
* @param {string} str The ASCII string to write.
*/
bitjs.io.ByteBuffer.prototype.writeASCIIString = function(str) {
for (var i = 0; i < str.length; ++i) {
var curByte = str.charCodeAt(i);
if (curByte < 0 || curByte > 255) {
throw "Trying to write a non-ASCII string!";
}
this.insertByte(curByte);
}
};
})();

View File

@ -1,195 +0,0 @@
/*
* bytestream.js
*
* Provides readers for byte streams.
*
* Licensed under the MIT License
*
* Copyright(c) 2011 Google Inc.
* Copyright(c) 2011 antimatter15
*/
/* global bitjs, Uint8Array */
var bitjs = bitjs || {};
bitjs.io = bitjs.io || {};
(function() {
/**
* This object allows you to peek and consume bytes as numbers and strings
* out of an ArrayBuffer. In this buffer, everything must be byte-aligned.
*
* @param {ArrayBuffer} ab The ArrayBuffer object.
* @param {number=} optOffset The offset into the ArrayBuffer
* @param {number=} optLength The length of this BitStream
* @constructor
*/
bitjs.io.ByteStream = function(ab, optOffset, optLength) {
var offset = optOffset || 0;
var length = optLength || ab.byteLength;
this.bytes = new Uint8Array(ab, offset, length);
this.ptr = 0;
};
/**
* Peeks at the next n bytes as an unsigned number but does not advance the
* pointer
* TODO: This apparently cannot read more than 4 bytes as a number?
* @param {number} n The number of bytes to peek at.
* @return {number} The n bytes interpreted as an unsigned number.
*/
bitjs.io.ByteStream.prototype.peekNumber = function(n) {
// TODO: return error if n would go past the end of the stream?
if (n <= 0 || typeof n !== typeof 1) {
return -1;
}
var result = 0;
// read from last byte to first byte and roll them in
var curByte = this.ptr + n - 1;
while (curByte >= this.ptr) {
result <<= 8;
result |= this.bytes[curByte];
--curByte;
}
return result;
};
/**
* Returns the next n bytes as an unsigned number (or -1 on error)
* and advances the stream pointer n bytes.
* @param {number} n The number of bytes to read.
* @return {number} The n bytes interpreted as an unsigned number.
*/
bitjs.io.ByteStream.prototype.readNumber = function(n) {
var num = this.peekNumber(n);
this.ptr += n;
return num;
};
/**
* Returns the next n bytes as a signed number but does not advance the
* pointer.
* @param {number} n The number of bytes to read.
* @return {number} The bytes interpreted as a signed number.
*/
bitjs.io.ByteStream.prototype.peekSignedNumber = function(n) {
var num = this.peekNumber(n);
var HALF = Math.pow(2, (n * 8) - 1);
var FULL = HALF * 2;
if (num >= HALF) num -= FULL;
return num;
};
/**
* Returns the next n bytes as a signed number and advances the stream pointer.
* @param {number} n The number of bytes to read.
* @return {number} The bytes interpreted as a signed number.
*/
bitjs.io.ByteStream.prototype.readSignedNumber = function(n) {
var num = this.peekSignedNumber(n);
this.ptr += n;
return num;
};
/**
* ToDo: Returns the next n bytes as a signed number and advances the stream pointer.
* @param {number} n The number of bytes to read.
* @return {number} The bytes interpreted as a signed number.
*/
bitjs.io.ByteStream.prototype.movePointer = function(n) {
this.ptr += n;
// end of buffer reached
if ((this.bytes.byteLength - this.ptr) < 0 ) {
this.ptr = this.bytes.byteLength;
}
}
/**
* ToDo: Returns the next n bytes as a signed number and advances the stream pointer.
* @param {number} n The number of bytes to read.
* @return {number} The bytes interpreted as a signed number.
*/
bitjs.io.ByteStream.prototype.moveTo = function(n) {
if ( n < 0 ) {
n = 0;
}
this.ptr = n;
// end of buffer reached
if ((this.bytes.byteLength - this.ptr) < 0 ) {
this.ptr = this.bytes.byteLength;
}
}
/**
* This returns n bytes as a sub-array, advancing the pointer if movePointers
* is true.
* @param {number} n The number of bytes to read.
* @param {boolean} movePointers Whether to move the pointers.
* @return {Uint8Array} The subarray.
*/
bitjs.io.ByteStream.prototype.peekBytes = function(n, movePointers) {
if (n <= 0 || typeof n !== typeof 1) {
return null;
}
var result = this.bytes.subarray(this.ptr, this.ptr + n);
if (movePointers) {
this.ptr += n;
}
return result;
};
/**
* Reads the next n bytes as a sub-array.
* @param {number} n The number of bytes to read.
* @return {Uint8Array} The subarray.
*/
bitjs.io.ByteStream.prototype.readBytes = function(n) {
return this.peekBytes(n, true);
};
/**
* Peeks at the next n bytes as a string but does not advance the pointer.
* @param {number} n The number of bytes to peek at.
* @return {string} The next n bytes as a string.
*/
bitjs.io.ByteStream.prototype.peekString = function(n) {
if (n <= 0 || typeof n !== typeof 1) {
return "";
}
var result = "";
for (var p = this.ptr, end = this.ptr + n; p < end; ++p) {
result += String.fromCharCode(this.bytes[p]);
}
return result;
};
/**
* Returns the next n bytes as an ASCII string and advances the stream pointer
* n bytes.
* @param {number} n The number of bytes to read.
* @return {string} The next n bytes as a string.
*/
bitjs.io.ByteStream.prototype.readString = function(n) {
var strToReturn = this.peekString(n);
this.ptr += n;
return strToReturn;
};
})();

View File

@ -15,7 +15,7 @@
* Typed Arrays: http://www.khronos.org/registry/typedarray/specs/latest/#6
*/
/* global screenfull, bitjs, Uint8Array, opera */
/* global screenfull, bitjs, Uint8Array, opera, loadArchiveFormats, archiveOpenFile */
/* exported init, event */
@ -104,9 +104,8 @@ kthoom.setSettings = function() {
};
var createURLFromArray = function(array, mimeType) {
var offset = array.byteOffset;
var offset = 0; // array.byteOffset;
var len = array.byteLength;
// var url;
var blob;
if (mimeType === "image/xml+svg") {
@ -166,93 +165,61 @@ kthoom.ImageFile = function(file) {
}
if ( this.mimeType !== undefined) {
this.dataURI = createURLFromArray(file.fileData, this.mimeType);
this.data = file;
}
};
function initProgressClick() {
$("#progress").click(function(e) {
var offset = $(this).offset();
var x = e.pageX - offset.left;
var rate = settings.direction === 0 ? x / $(this).width() : 1 - x / $(this).width();
var page = Math.max(1, Math.ceil(rate * totalImages)) - 1;
currentImage = page;
currentImage = Math.max(1, Math.ceil(rate * totalImages)) - 1;
updatePage();
});
}
function loadFromArrayBuffer(ab) {
var start = (new Date).getTime();
var h = new Uint8Array(ab, 0, 10);
var pathToBitJS = "../../static/js/archive/";
var lastCompletion = 0;
if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { //Rar!
unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS);
} else if (h[0] === 80 && h[1] === 75) { //PK (Zip)
unarchiver = new bitjs.archive.Unzipper(ab, pathToBitJS);
} else if (h[0] === 255 && h[1] === 216) { // JPEG
// ToDo: check
updateProgress(100);
lastCompletion = 100;
return;
} else { // Try with tar
unarchiver = new bitjs.archive.Untarrer(ab, pathToBitJS);
}
// Listen for UnarchiveEvents.
if (unarchiver) {
unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.PROGRESS,
function(e) {
var percentage = e.currentBytesUnarchived / e.totalUncompressedBytesInArchive;
if (totalImages === 0) {
totalImages = e.totalFilesInArchive;
}
updateProgress(percentage * 100);
lastCompletion = percentage * 100;
});
unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.INFO,
function(e) {
// console.log(e.msg); // Enable debug output here
});
unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.EXTRACT,
function(e) {
// convert DecompressedFile into a bunch of ImageFiles
if (e.unarchivedFile) {
var f = e.unarchivedFile;
// add any new pages based on the filename
if (imageFilenames.indexOf(f.filename) === -1) {
var test = new kthoom.ImageFile(f);
if ( test.mimeType !== undefined) {
imageFilenames.push(f.filename);
imageFiles.push(test);
// add thumbnails to the TOC list
$("#thumbnails").append(
"<li>" +
"<a data-page='" + imageFiles.length + "'>" +
loadArchiveFormats(['rar', 'zip', 'tar'], function() {
// Open the file as an archive
archiveOpenFile(ab, function (archive) {
if (archive) {
totalImages = archive.entries.length
console.info('Uncompressing ' + archive.archive_type + ' ...');
archive.entries.forEach(function(e, i) {
updateProgress( (i + 1)/ totalImages * 100);
if (e.is_file) {
e.readData(function(d) {
// add any new pages based on the filename
if (imageFilenames.indexOf(e.name) === -1) {
let data = {filename: e.name, fileData: d};
var test = new kthoom.ImageFile(data);
if (test.mimeType !== undefined) {
imageFilenames.push(e.name);
imageFiles.push(test);
// add thumbnails to the TOC list
$("#thumbnails").append(
"<li>" +
"<a data-page='" + imageFiles.length + "'>" +
"<img src='" + imageFiles[imageFiles.length - 1].dataURI + "'/>" +
"<span>" + imageFiles.length + "</span>" +
"</a>" +
"</li>"
);
// display first page if we haven't yet
if (imageFiles.length === currentImage + 1) {
updatePage(lastCompletion);
"</a>" +
"</li>"
);
// display first page if we haven't yet
if (imageFiles.length === currentImage + 1) {
updatePage(lastCompletion);
}
} else {
totalImages--;
}
}
} else {
totalImages--;
}
});
}
}
});
unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.FINISH,
function() {
var diff = ((new Date).getTime() - start) / 1000;
console.log("Unarchiving done in " + diff + "s");
});
unarchiver.start();
} else {
alert("Some error");
}
});
}
});
});
}
function scrollTocToActive() {
@ -317,7 +284,6 @@ function updateProgress(loadPercentage) {
.find(".load").text("");
}
}
// Set page progress bar
$("#progress .bar-read").css({ width: totalImages === 0 ? 0 : Math.round((currentImage + 1) / totalImages * 100) + "%"});
}
@ -553,10 +519,6 @@ function keyHandler(evt) {
updateScale(false);
break;
case kthoom.Key.SPACE:
var container = $("#mainContent");
// var atTop = container.scrollTop() === 0;
// var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height();
if (evt.shiftKey) {
evt.preventDefault();
// If it's Shift + Space and the container is at the top of the page
@ -573,33 +535,11 @@ function keyHandler(evt) {
}
}
/*function ImageLoadCallback() {
var jso = this.response;
// Unable to decompress file, or no response from server
if (jso === null) {
setImage("error");
} else {
// IE 11 sometimes sees the response as a string
if (typeof jso !== "object") {
jso = JSON.parse(jso);
}
if (jso.page !== jso.last) {
this.open("GET", this.fileid + "/" + (jso.page + 1));
this.addEventListener("load", ImageLoadCallback);
this.send();
}
loadFromArrayBuffer(jso);
}
}*/
function init(filename) {
var request = new XMLHttpRequest();
request.open("GET", filename);
request.responseType = "arraybuffer";
request.setRequestHeader("X-Test", "test1");
request.setRequestHeader("X-Test", "test2");
request.addEventListener("load", function(event) {
request.addEventListener("load", function() {
if (request.status >= 200 && request.status < 300) {
loadFromArrayBuffer(request.response);
} else {

View File

@ -141,7 +141,7 @@ function confirmDialog(id, dialogid, dataValue, yesFn, noFn) {
$confirm.modal("hide");
});
$.ajax({
method:"get",
method:"post",
dataType: "json",
url: getPath() + "/ajax/loaddialogtexts/" + id,
success: function success(data) {
@ -179,18 +179,6 @@ $("#delete_confirm").click(function() {
}
});
$("#books-table").bootstrapTable("refresh");
/*$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/listbooks",
async: true,
timeout: 900,
success:function(data) {
$("#book-table").bootstrapTable("load", data);
loadSuccess();
}
});*/
}
});
} else {
@ -218,8 +206,6 @@ $("#deleteModal").on("show.bs.modal", function(e) {
$(e.currentTarget).find("#delete_confirm").data("ajax", $(e.relatedTarget).data("ajax"));
});
$(function() {
var updateTimerID;
var updateText;
@ -556,6 +542,86 @@ $(function() {
this.closest("form").submit();
});
function handle_response(data) {
if (!jQuery.isEmptyObject(data)) {
data.forEach(function (item) {
$(".navbar").after('<div class="row-fluid text-center" style="margin-top: -20px;">' +
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
'</div>');
});
}
}
$('.collapse').on('shown.bs.collapse', function(){
$(this).parent().find(".glyphicon-plus").removeClass("glyphicon-plus").addClass("glyphicon-minus");
}).on('hidden.bs.collapse', function(){
$(this).parent().find(".glyphicon-minus").removeClass("glyphicon-minus").addClass("glyphicon-plus");
});
function changeDbSettings() {
$("#db_submit").closest('form').submit();
}
$("#db_submit").click(function(e) {
e.preventDefault();
e.stopPropagation();
this.blur();
$.ajax({
method:"post",
dataType: "json",
url: window.location.pathname + "/../../ajax/simulatedbchange",
data: {config_calibre_dir: $("#config_calibre_dir").val()},
success: function success(data) {
if ( data.change ) {
if ( data.valid ) {
confirmDialog(
"db_submit",
"GeneralChangeModal",
0,
changeDbSettings
);
}
else {
$("#InvalidDialog").modal('show');
}
} else {
changeDbSettings();
}
}
});
});
$("#config_submit").click(function(e) {
e.preventDefault();
e.stopPropagation();
this.blur();
window.scrollTo({top: 0, behavior: 'smooth'});
var request_path = "/../../admin/ajaxconfig";
var loader = "/../..";
$("#flash_success").remove();
$("#flash_danger").remove();
$.post(window.location.pathname + request_path, $(this).closest("form").serialize(), function(data) {
$('#config_upload_formats').val(data.config_upload);
if(data.reboot) {
$("#spinning_success").show();
var rebootInterval = setInterval(function(){
$.get({
url:window.location.pathname + "/../../admin/alive",
success: function (d, statusText, xhr) {
if (xhr.status < 400) {
$("#spinning_success").hide();
clearInterval(rebootInterval);
handle_response(data.result);
}
},
});
}, 1000);
} else {
handle_response(data.result);
}
});
});
$("#delete_shelf").click(function() {
confirmDialog(
$(this).attr('id'),
@ -568,7 +634,6 @@ $(function() {
});
$("#fileModal").on("show.bs.modal", function(e) {
var target = $(e.relatedTarget);
var path = $("#" + target.data("link"))[0].value;
@ -632,7 +697,6 @@ $(function() {
$(".update-view").click(function(e) {
var view = $(this).data("view");
e.preventDefault();
e.stopPropagation();
$.ajax({

View File

@ -46,9 +46,14 @@ $(function() {
if (selections.length < 1) {
$("#delete_selection").addClass("disabled");
$("#delete_selection").attr("aria-disabled", true);
$("#table_xchange").addClass("disabled");
$("#table_xchange").attr("aria-disabled", true);
} else {
$("#delete_selection").removeClass("disabled");
$("#delete_selection").attr("aria-disabled", false);
$("#table_xchange").removeClass("disabled");
$("#table_xchange").attr("aria-disabled", false);
}
});
$("#delete_selection").click(function() {
@ -86,6 +91,20 @@ $(function() {
});
});
$("#table_xchange").click(function() {
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/xchange",
data: JSON.stringify({"xchange":selections}),
success: function success() {
$("#books-table").bootstrapTable("refresh");
$("#books-table").bootstrapTable("uncheckAll");
}
});
});
var column = [];
$("#books-table > thead > tr > th").each(function() {
var element = {};
@ -580,12 +599,19 @@ function singleUserFormatter(value, row) {
return '<a class="btn btn-default" onclick="storeLocation()" href="' + window.location.pathname + '/../../admin/user/' + row.id + '">' + this.buttontext + '</a>'
}
function checkboxFormatter(value, row, index){
function checkboxFormatter(value, row){
if(value & this.column)
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" checked onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', ' + this.column + ')">';
else
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', ' + this.column + ')">';
}
function singlecheckboxFormatter(value, row){
if(value)
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" checked onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', 0)">';
else
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', 0)">';
}
/* Do some hiding disabling after user list is loaded */
function loadSuccess() {

View File

@ -124,15 +124,24 @@
error: function(xhr) {
this.$modalTitle.text(this.options.modalTitleFailed);
this.setProgress(100);
this.$modalBar.removeClass("progress-bar-success");
this.$modalBar.addClass("progress-bar-danger");
this.$modalFooter.show();
var contentType = xhr.getResponseHeader("Content-Type");
// Write the error response to the document.
if (contentType || xhr.status === 422) {
if (xhr.status === 502 || xhr.status === 0) {
if (xhr.statusText) {
this.$modalBar.text(xhr.statusText + ": File size may be too big");
} else {
this.$modalBar.text("Error: File size may be too big");
}
}
else if (contentType || xhr.status === 422) {
var responseText = xhr.responseText;
if (contentType.indexOf("text/plain") !== -1) {
if (contentType.indexOf("text/plain") === -1) {
responseText = "<pre>" + responseText + "</pre>";
document.write(responseText);
} else {

View File

@ -150,6 +150,7 @@
</div>
{% endif %}
</div>
<a class="btn btn-default" id="db_config" href="{{url_for('admin.db_configuration')}}">{{_('Edit Calibre Database Configuration')}}</a>
<a class="btn btn-default" id="basic_config" href="{{url_for('admin.configuration')}}">{{_('Edit Basic Configuration')}}</a>
<a class="btn btn-default" id="view_config" href="{{url_for('admin.view_configuration')}}">{{_('Edit UI Configuration')}}</a>
</div>

View File

@ -3,7 +3,7 @@
{% if book %}
<div class="col-sm-3 col-lg-3 col-xs-12">
<div class="cover">
<img src="{{ url_for('web.get_cover', book_id=book.id, edit=1|uuidfilter) }}" alt="{{ book.title }}"/>
<img id="detailcover" src="{{ url_for('web.get_cover', book_id=book.id, edit=1|uuidfilter) }}" alt="{{ book.title }}"/>
</div>
{% if g.user.role_delete_books() %}
<div class="text-center">
@ -53,6 +53,10 @@
<label for="book_title">{{_('Book Title')}}</label>
<input type="text" class="form-control" name="book_title" id="book_title" value="{{book.title}}">
</div>
<div class="text-center">
<button type="button" class="btn btn-default" id="xchange" ><span class="glyphicon glyphicon-arrow-up"></span><span class="glyphicon glyphicon-arrow-down"></span></button>
</div>
<div class="form-group">
<label for="bookAuthor">{{_('Author')}}</label>
<input type="text" class="form-control typeahead" name="author_name" id="bookAuthor" value="{{' & '.join(authors)}}" autocomplete="off">
@ -327,6 +331,7 @@
<script src="{{ url_for('static', filename='js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.' + g.user.locale + '.min.js') }}" charset="UTF-8"></script>
{% endif %}
<script src="{{ url_for('static', filename='js/edit_books.js') }}"></script>
<script src="{{ url_for('static', filename='js/fullscreen.js') }}"></script>
{% endblock %}
{% block header %}
<meta name="referrer" content="never">

View File

@ -20,17 +20,20 @@
{% block body %}
<h2 class="{{page}}">{{_(title)}}</h2>
<div class="col-xs-12 col-sm-6">
<div class="row">
<div class="row form-group">
<div class="btn btn-default disabled" id="merge_books" data-toggle="modal" data-target="#mergeModal" aria-disabled="true">{{_('Merge selected books')}}</div>
<div class="btn btn-default disabled" id="delete_selection" aria-disabled="true">{{_('Remove Selections')}}</div>
</div>
<div class="row form-group">
<div class="btn btn-default disabled" id="table_xchange" ><span class="glyphicon glyphicon-arrow-up"></span><span class="glyphicon glyphicon-arrow-down"></span>{{_('Exchange author and title')}}</div>
</div>
</div>
<div class="col-xs-12 col-sm-6">
<div class="row">
<div class="filterheader col-xs-12 col-sm-6">
<div class="row form-group">
<input type="checkbox" id="autoupdate_titlesort" name="autoupdate_titlesort" checked>
<label for="autoupdate_titlesort">{{_('Update Title Sort automatically')}}</label>
</div>
<div class="row">
<div class="row form-group">
<input type="checkbox" id="autoupdate_authorsort" name="autoupdate_authorsort" checked>
<label for="autoupdate_authorsort">{{_('Update Author Sort automatically')}}</label>
</div>

View File

@ -0,0 +1,74 @@
{% extends "layout.html" %}
{% block flash %}
<div id="spinning_success" class="row-fluid text-center" style="margin-top: -20px; display:none;">
<div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/></div>
</div>
{% endblock %}
{% block body %}
<div class="discover">
<h2>{{title}}</h2>
<form role="form" method="POST" class="col-md-10 col-lg-6" action="{{ url_for('admin.db_configuration') }}" autocomplete="off">
<label for="config_calibre_dir">{{_('Location of Calibre Database')}}</label>
<div class="form-group required input-group">
<input type="text" class="form-control" id="config_calibre_dir" name="config_calibre_dir" value="{% if config.config_calibre_dir != None %}{{ config.config_calibre_dir }}{% endif %}" autocomplete="off">
<span class="input-group-btn">
<button type="button" data-toggle="modal" id="calibre_modal_path" data-link="config_calibre_dir" data-filefilter="metadata.db" data-target="#fileModal" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
{% if feature_support['gdrive'] %}
<div class="form-group required">
<input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" data-control="gdrive_settings" {% if config.config_use_google_drive %}checked{% endif %} >
<label for="config_use_google_drive">{{_('Use Google Drive?')}}</label>
</div>
{% if not gdriveError %}
{% if show_authenticate_google_drive and config.config_use_google_drive %}
<div class="form-group required">
<a href="{{ url_for('gdrive.authenticate_google_drive') }}" id="gdrive_auth" class="btn btn-primary">{{_('Authenticate Google Drive')}}</a>
</div>
{% else %}
{% if not show_authenticate_google_drive %}
<div class="form-group required">
<label for="config_google_drive_folder">{{_('Google Drive Calibre folder')}}</label>
<select name="config_google_drive_folder" id="config_google_drive_folder" class="form-control">
{% for gdrivefolder in gdrivefolders %}
<option value="{{ gdrivefolder.title }}" {% if gdrivefolder.title == config.config_google_drive_folder %}selected{% endif %}>{{ gdrivefolder.title }}</option>
{% endfor %}
</select>
</div>
{% if config.config_google_drive_watch_changes_response %}
<label for="config_google_drive_watch_changes_response">{{_('Metadata Watch Channel ID')}}</label>
<div class="form-group input-group required">
<input type="text" class="form-control" name="config_google_drive_watch_changes_response" id="config_google_drive_watch_changes_response" value="{{ config.config_google_drive_watch_changes_response['id'] }} expires on {{ config.config_google_drive_watch_changes_response['expiration'] | strftime }}" autocomplete="off" disabled="">
<span class="input-group-btn"><a href="{{ url_for('gdrive.revoke_watch_gdrive') }}" id="watch_revoke" class="btn btn-primary">{{_('Revoke')}}</a></span>
</div>
{% else %}
<a href="{{ url_for('gdrive.watch_gdrive') }}" id="enable_gdrive_watch" class="btn btn-primary">Enable watch of metadata.db</a>
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
<div class="col-sm-12">
<div id="db_submit" name="submit" class="btn btn-default">{{_('Save')}}</div>
<a href="{{ url_for('admin.admin') }}" id="config_back" class="btn btn-default">{{_('Cancel')}}</a>
</div>
</form>
</div>
{% endblock %}
{% block modal %}
{{ filechooser_modal() }}
{{ change_confirm_modal() }}
<div id="InvalidDialog" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header bg-info"></div>
<div class="modal-body text-center">
<p>{{_('New db location is invalid, please enter valid path')}}</p>
<p></p>
<button type="button" class="btn btn-default" id="invalid_confirm" data-dismiss="modal">{{_('OK')}}</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,97 +1,24 @@
{% extends "layout.html" %}
{% block flash %}
<div id="spinning_success" class="row-fluid text-center" style="margin-top: -20px; display:none;">
<div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/></div>
</div>
{% endblock %}
{% block body %}
<div class="discover">
<h2>{{title}}</h2>
<form role="form" method="POST" autocomplete="off">
<form role="form" method="POST" autocomplete="off">
<div class="panel-group col-md-10 col-lg-6">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" data-toggle="collapse" href="#collapseOne">
<span class="glyphicon glyphicon-minus"></span>
{{_('Library Configuration')}}
</a>
</h4>
</div>
<div id="collapseOne" class="panel-collapse collapse in">
<div class="panel-body">
<label for="config_calibre_dir">{{_('Location of Calibre Database')}}</label>
<div class="form-group required{% if filepicker %} input-group{% endif %}">
<input type="text" class="form-control" id="config_calibre_dir" name="config_calibre_dir" value="{% if config.config_calibre_dir != None %}{{ config.config_calibre_dir }}{% endif %}" autocomplete="off">
{% if filepicker %}
<span class="input-group-btn">
<button type="button" data-toggle="modal" id="calibre_modal_path" data-link="config_calibre_dir" data-filefilter="metadata.db" data-target="#fileModal" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
{% endif %}
</div>
{% if not filepicker %}
<div class="form-group">
<label id="filepicker-hint">{{_('To activate serverside filepicker start Calibre-Web with -f option')}}</label>
</div>
{% endif %}
{% if feature_support['gdrive'] %}
<div class="form-group required">
<input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" data-control="gdrive_settings" {% if config.config_use_google_drive %}checked{% endif %} >
<label for="config_use_google_drive">{{_('Use Google Drive?')}}</label>
</div>
<div data-related="gdrive_settings">
{% if gdriveError %}
<div class="form-group">
<label id="gdrive_error">
{{_('Google Drive config problem')}}: {{ gdriveError }}
</label>
</div>
{% else %}
{% if show_authenticate_google_drive and g.user.is_authenticated and config.config_use_google_drive %}
<div class="form-group required">
<a href="{{ url_for('gdrive.authenticate_google_drive') }}" id="gdrive_auth" class="btn btn-primary">{{_('Authenticate Google Drive')}}</a>
</div>
{% else %}
{% if show_authenticate_google_drive and g.user.is_authenticated and not config.config_use_google_drive %}
<div >{{_('Please hit save to continue with setup')}}</div>
{% endif %}
{% if not g.user.is_authenticated and show_login_button %}
<div >{{_('Please finish Google Drive setup after login')}}</div>
{% endif %}
{% if g.user.is_authenticated %}
{% if not show_authenticate_google_drive %}
<div class="form-group required">
<label for="config_google_drive_folder">{{_('Google Drive Calibre folder')}}</label>
<select name="config_google_drive_folder" id="config_google_drive_folder" class="form-control">
{% for gdrivefolder in gdrivefolders %}
<option value="{{ gdrivefolder.title }}" {% if gdrivefolder.title == config.config_google_drive_folder %}selected{% endif %}>{{ gdrivefolder.title }}</option>
{% endfor %}
</select>
</div>
{% if config.config_google_drive_watch_changes_response %}
<label for="config_google_drive_watch_changes_response">{{_('Metadata Watch Channel ID')}}</label>
<div class="form-group input-group required">
<input type="text" class="form-control" name="config_google_drive_watch_changes_response" id="config_google_drive_watch_changes_response" value="{{ config.config_google_drive_watch_changes_response['id'] }} expires on {{ config.config_google_drive_watch_changes_response['expiration'] | strftime }}" autocomplete="off" disabled="">
<span class="input-group-btn"><a href="{{ url_for('gdrive.revoke_watch_gdrive') }}" id="watch_revoke" class="btn btn-primary">{{_('Revoke')}}</a></span>
</div>
{% else %}
<a href="{{ url_for('gdrive.watch_gdrive') }}" id="enable_gdrive_watch" class="btn btn-primary">Enable watch of metadata.db</a>
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
{% if show_back_button %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" data-toggle="collapse" href="#collapsetwo">
<a class="accordion-toggle" data-toggle="collapse" href="#collapseone">
<span class="glyphicon glyphicon-plus"></span>
{{_('Server Configuration')}}
</a>
</h4>
</div>
<div id="collapsetwo" class="panel-collapse collapse">
<div id="collapseone" class="panel-collapse collapse">
<div class="panel-body">
<div class="form-group">
<label for="config_port">{{_('Server Port')}}</label>
@ -124,13 +51,13 @@
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" data-toggle="collapse" href="#collapsethree">
<a class="accordion-toggle" data-toggle="collapse" href="#collapsetwo">
<span class="glyphicon glyphicon-plus"></span>
{{_('Logfile Configuration')}}
</a>
</h4>
</div>
<div id="collapsethree" class="panel-collapse collapse">
<div id="collapsetwo" class="panel-collapse collapse">
<div class="panel-body">
<div class="form-group">
<label for="config_log_level">{{_('Log Level')}}</label>
@ -159,13 +86,13 @@
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" data-toggle="collapse" href="#collapsefive">
<a class="accordion-toggle" data-toggle="collapse" href="#collapsefour">
<span class="glyphicon glyphicon-plus"></span>
{{_('Feature Configuration')}}
</a>
</h4>
</div>
<div id="collapsefive" class="panel-collapse collapse">
<div id="collapsefour" class="panel-collapse collapse">
<div class="panel-body">
<div class="form-group">
<input type="checkbox" id="config_uploading" data-control="upload_settings" name="config_uploading" {% if config.config_uploading %}checked{% endif %}>
@ -379,13 +306,13 @@
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" data-toggle="collapse" href="#collapseeight">
<a class="accordion-toggle" data-toggle="collapse" href="#collapsefive">
<span class="glyphicon glyphicon-plus"></span>
{{_('External binaries')}}
</a>
</h4>
</div>
<div id="collapseeight" class="panel-collapse collapse">
<div id="collapsefive" class="panel-collapse collapse">
<div class="panel-body">
<label for="config_converterpath">{{_('Path to Calibre E-Book Converter')}}</label>
<div class="form-group input-group">
@ -417,18 +344,10 @@
</div>
</div>
</div>
{% endif %}
</div>
<div class="col-sm-12">
{% if not show_login_button %}
<button type="submit" name="submit" class="btn btn-default">{{_('Save')}}</button>
{% endif %}
{% if show_back_button %}
<a href="{{ url_for('admin.admin') }}" class="btn btn-default">{{_('Cancel')}}</a>
{% endif %}
{% if show_login_button %}
<a href="{{ url_for('web.login') }}" name="login" class="btn btn-default">{{_('Login')}}</a>
{% endif %}
<button type="button" name="submit" id="config_submit" class="btn btn-default">{{_('Save')}}</button>
<a href="{{ url_for('admin.admin') }}" id="config_back" class="btn btn-default">{{_('Cancel')}}</a>
</div>
</form>
</div>
@ -436,15 +355,3 @@
{% block modal %}
{{ filechooser_modal() }}
{% endblock %}
{% block js %}
<script type="text/javascript">
$(document).on('change', '#config_use_google_drive', function() {
$('#config_google_drive_folder').prop('required', $(this).prop('checked'));
});
$('.collapse').on('shown.bs.collapse', function(){
$(this).parent().find(".glyphicon-plus").removeClass("glyphicon-plus").addClass("glyphicon-minus");
}).on('hidden.bs.collapse', function(){
$(this).parent().find(".glyphicon-minus").removeClass("glyphicon-minus").addClass("glyphicon-plus");
});
</script>
{% endblock %}

View File

@ -6,8 +6,8 @@
{% block body %}
<div class="discover">
<h2>{{title}}</h2>
<form role="form" method="POST" autocomplete="off" class="col-md-10 col-lg-6">
<div class="panel-group">
<form role="form" method="POST" autocomplete="off" >
<div class="panel-group class="col-md-10 col-lg-6">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
@ -71,7 +71,6 @@
</div>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
@ -146,6 +145,7 @@
</div>
</div>
</div>
</div>
<div class="col-sm-12">
<button type="submit" name="submit" class="btn btn-default">{{_('Save')}}</button>
<a href="{{ url_for('admin.admin') }}" class="btn btn-default">{{_('Cancel')}}</a>
@ -157,13 +157,6 @@
{{ restrict_modal() }}
{% endblock %}
{% block js %}
<script type="text/javascript">
$('.collapse').on('shown.bs.collapse', function(){
$(this).parent().find(".glyphicon-plus").removeClass("glyphicon-plus").addClass("glyphicon-minus");
}).on('hidden.bs.collapse', function(){
$(this).parent().find(".glyphicon-minus").removeClass("glyphicon-minus").addClass("glyphicon-plus");
});
</script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>

View File

@ -4,7 +4,7 @@
<div class="row">
<div class="col-sm-3 col-lg-3 col-xs-5">
<div class="cover">
<img src="{{ url_for('web.get_cover', book_id=entry.id, edit=1|uuidfilter) }}" alt="{{ entry.title }}" />
<img id="detailcover" src="{{ url_for('web.get_cover', book_id=entry.id, edit=1|uuidfilter) }}" alt="{{ entry.title }}" />
</div>
</div>
<div class="col-sm-9 col-lg-9 book-meta">
@ -316,4 +316,5 @@
</a>
</script>
<script src="{{ url_for('static', filename='js/details.js') }}"></script>
<script src="{{ url_for('static', filename='js/fullscreen.js') }}"></script>
{% endblock %}

View File

@ -27,6 +27,9 @@
</div>
<div class="row">
<div class="col-md-offset-4 text-left">
{% if unconfigured %}
<div>{{_('Calibre-Web Instance is unconfigured, please contact your administrator')}}</div>
{% endif %}
{% for element in error_stack %}
<div>{{ element }}</div>
{% endfor %}
@ -39,13 +42,15 @@
</div>
</div>
{% endif %}
<div class="row">
<div class="col errorlink">
<div class="row">
<div class="col errorlink">
{% if not unconfigured %}
<a href="{{url_for('web.index')}}" title="{{ _('Return to Home') }}">{{_('Return to Home')}}</a>
{% else %}
<a href="{{url_for('web.logout')}}" title="{{ _('Logout User') }}">{{ _('Logout User') }}</a>
{% endif %}
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -111,6 +111,7 @@
</div>
{%endif%}
{% endfor %}
{% block flash %}{% endblock %}
{% if g.current_theme == 1 %}
<div id="loader" hidden="true">
<center>

View File

@ -15,8 +15,8 @@
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/plugins.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/compress/uncompress.js') }}"></script>
<script src="{{ url_for('static', filename='js/kthoom.js') }}"></script>
<script src="{{ url_for('static', filename='js/archive/archive.js') }}"></script>
<script>
var updateArrows = function() {
if ($('input[name="direction"]:checked').val() === "0") {

View File

@ -14,6 +14,13 @@
</label>
</div>
{% endif %}
{% if kobo_sync_enabled and sync_only_selected_shelves %}
<div class="checkbox">
<label> <input type="checkbox" name="kobo_sync" {% if shelf.kobo_sync == 1 %}checked{% endif %}>
{{ _('Sync this shelf with Kobo device') }}
</label>
</div>
{% endif %}
<button type="submit" class="btn btn-default" id="submit">{{_('Save')}}</button>
{% if shelf.id != None %}
<a href="{{ url_for('shelf.show_shelf', shelf_id=shelf.id) }}" class="btn btn-default">{{_('Cancel')}}</a>

View File

@ -47,7 +47,7 @@
{% endfor %}
</select>
</div>
{% if registered_oauth.keys()| length > 0 and not new_user %}
{% if registered_oauth.keys()| length > 0 and not new_user and profile %}
{% for id, name in registered_oauth.items() %}
<div class="form-group">
<label>{{ name }} {{_('OAuth Settings')}}</label>
@ -66,7 +66,6 @@
<div class="btn btn-danger" id="config_delete_kobo_token" data-value="{{ content.id }}" data-remote="false" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Delete')}}</div>
</div>
{% endif %}
</div>
<div class="col-sm-6">
{% for element in sidebar %}
{% if element['config_show'] %}
@ -125,6 +124,12 @@
</div>
{% endif %}
{% endif %}
{% if kobo_support and not content.role_anonymous() %}
<div class="form-group">
<input type="checkbox" name="kobo_only_shelves_sync" id="kobo_only_shelves_sync" {% if content.kobo_only_shelves_sync %}checked{% endif %}>
<label for="kobo_only_shelves_sync">{{_('Sync only books in selected shelves with Kobo')}}</label>
</div>
{% endif %}
</div>
<div class="col-sm-12">
<div id="user_submit" class="btn btn-default">{{_('Save')}}</div>

View File

@ -32,6 +32,21 @@
</th>
{%- endmacro %}
{% macro user_single_checkbox_row(parameter, show_text) -%}
<th data-name="{{parameter}}" data-field="{{parameter}}"
data-formatter="singlecheckboxFormatter">
<div class="form-check">
<div>
<input type="radio" class="check_head" data-set="false" data-val="0" name="{{parameter}}" id="false_{{parameter}}" data-name="{{parameter}}" disabled>{{_('Deny')}}
</div>
<div>
<input type="radio" class="check_head" data-set="true" data-val="1" name="{{parameter}}" data-name="{{parameter}}" disabled>{{_('Allow')}}
</div>
</div>
{{show_text}}
</th>
{%- endmacro %}
{% macro user_checkbox_row(parameter, array_field, show_text, element, value) -%}
<th data-name="{{array_field}}" data-field="{{parameter}}"
data-visible="{{element.get(array_field)}}"
@ -39,14 +54,10 @@
data-formatter="checkboxFormatter">
<div class="form-check">
<div>
<input type="radio" class="check_head" data-set="false" data-val={{value.get(array_field)}} name="options_{{array_field}}" id="false_{{array_field}}" data-name="{{parameter}}" disabled>{{_('Deny')}}
<input type="radio" class="check_head" data-set="false" data-val="{{value.get(array_field)}}" name="options_{{array_field}}" id="false_{{array_field}}" data-name="{{parameter}}" disabled>{{_('Deny')}}
</div>
<div>
<input type="radio" class="check_head" data-set="true" data-val={{value.get(array_field)}} name="options_{{array_field}}" data-name="{{parameter}}" disabled>{{_('Allow')}}
<input type="radio" class="check_head" data-set="true" data-val="{{value.get(array_field)}}" name="options_{{array_field}}" data-name="{{parameter}}" disabled>{{_('Allow')}}
</div>
</div>
{{show_text}}
@ -134,7 +145,10 @@
{{ user_checkbox_row("role", "viewer_role", _('View'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "edit_role", _('Edit'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "delete_role", _('Delete'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "edit_shelf_role", _('Edit Public Shelfs'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "edit_shelf_role", _('Edit Public Shelves'), visiblility, all_roles)}}
{% if kobo_support %}
{{ user_single_checkbox_row("kobo_only_shelves_sync", _('Sync Selected Shelves with Kobo'))}}
{% endif %}
{{ user_checkbox_row("sidebar_view", "detail_random", _('Show Random Books in Detail View'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_language", _('Show language selection'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_read_and_unread", _('Show read/unread selection'), visiblility, sidebar_settings)}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -188,7 +188,7 @@ class User(UserBase, Base):
allowed_column_value = Column(String, default="")
remote_auth_token = relationship('RemoteAuthToken', backref='user', lazy='dynamic')
view_settings = Column(JSON, default={})
kobo_only_shelves_sync = Column(Integer, default=0)
if oauth_support:
@ -229,6 +229,7 @@ class Anonymous(AnonymousUserMixin, UserBase):
self.denied_column_value = data.denied_column_value
self.allowed_column_value = data.allowed_column_value
self.view_settings = data.view_settings
self.kobo_only_shelves_sync = data.kobo_only_shelves_sync
def role_admin(self):
@ -270,6 +271,7 @@ class Shelf(Base):
name = Column(String)
is_public = Column(Integer, default=0)
user_id = Column(Integer, ForeignKey('user.id'))
kobo_sync = Column(Boolean, default=False)
books = relationship("BookShelf", backref="ub_shelf", cascade="all, delete-orphan", lazy="dynamic")
created = Column(DateTime, default=datetime.datetime.utcnow)
last_modified = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
@ -483,11 +485,12 @@ def migrate_registration_table(engine, session):
# Remove login capability of user Guest
def migrate_guest_password(engine, session):
def migrate_guest_password(engine):
try:
with engine.connect() as conn:
trans = conn.begin()
conn.execute(text("UPDATE user SET password='' where name = 'Guest' and password !=''"))
session.commit()
trans.commit()
except exc.OperationalError:
print('Settings database is not writeable. Exiting...')
sys.exit(2)
@ -502,6 +505,7 @@ def migrate_shelfs(engine, session):
conn.execute("ALTER TABLE shelf ADD column 'created' DATETIME")
conn.execute("ALTER TABLE shelf ADD column 'last_modified' DATETIME")
conn.execute("ALTER TABLE book_shelf_link ADD column 'date_added' DATETIME")
conn.execute("ALTER TABLE shelf ADD column 'kobo_sync' BOOLEAN DEFAULT false")
for shelf in session.query(Shelf).all():
shelf.uuid = str(uuid.uuid4())
shelf.created = datetime.datetime.now()
@ -509,6 +513,15 @@ def migrate_shelfs(engine, session):
for book_shelf in session.query(BookShelf).all():
book_shelf.date_added = datetime.datetime.now()
session.commit()
try:
session.query(exists().where(Shelf.kobo_sync)).scalar()
except exc.OperationalError:
with engine.connect() as conn:
conn.execute("ALTER TABLE shelf ADD column 'kobo_sync' BOOLEAN DEFAULT false")
session.commit()
try:
session.query(exists().where(BookShelf.order)).scalar()
except exc.OperationalError: # Database is not compatible, some columns are missing
@ -592,6 +605,13 @@ def migrate_Database(session):
with engine.connect() as conn:
conn.execute("ALTER TABLE user ADD column `view_settings` VARCHAR(10) DEFAULT '{}'")
session.commit()
try:
session.query(exists().where(User.kobo_only_shelves_sync)).scalar()
except exc.OperationalError:
with engine.connect() as conn:
conn.execute("ALTER TABLE user ADD column `kobo_only_shelves_sync` SMALLINT DEFAULT 0")
session.commit()
try:
# check if name is in User table instead of nickname
session.query(exists().where(User.name)).scalar()
@ -611,15 +631,16 @@ def migrate_Database(session):
"allowed_tags VARCHAR,"
"denied_column_value VARCHAR,"
"allowed_column_value VARCHAR,"
"view_settings JSON,"
"view_settings JSON,"
"kobo_only_shelves_sync SMALLINT,"
"UNIQUE (name),"
"UNIQUE (email))"))
conn.execute(text("INSERT INTO user_id(id, name, email, role, password, kindle_mail,locale,"
"sidebar_view, default_language, denied_tags, allowed_tags, denied_column_value, "
"allowed_column_value, view_settings)"
"allowed_column_value, view_settings, kobo_only_shelves_sync)"
"SELECT id, nickname, email, role, password, kindle_mail, locale,"
"sidebar_view, default_language, denied_tags, allowed_tags, denied_column_value, "
"allowed_column_value, view_settings FROM user"))
"allowed_column_value, view_settings, kobo_only_shelves_sync FROM user"))
# delete old user table and rename new user_id table to user:
conn.execute(text("DROP TABLE user"))
conn.execute(text("ALTER TABLE user_id RENAME TO user"))
@ -628,7 +649,7 @@ def migrate_Database(session):
is None:
create_anonymous_user(session)
migrate_guest_password(engine, session)
migrate_guest_password(engine)
def clean_database(session):

View File

@ -185,7 +185,7 @@ class Updater(threading.Thread):
def moveallfiles(cls, root_src_dir, root_dst_dir):
new_permissions = os.stat(root_dst_dir)
log.debug('Performing Update on OS-System: %s', sys.platform)
change_permissions = (sys.platform == "win32" or sys.platform == "darwin")
change_permissions = not (sys.platform == "win32" or sys.platform == "darwin")
for src_dir, __, files in os.walk(root_src_dir):
dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
if not os.path.exists(dst_dir):

View File

@ -1211,7 +1211,7 @@ def extend_search_term(searchterm,
for key, db_element in elements.items():
tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['include_' + key])).all()
searchterm.extend(tag.name for tag in tag_names)
tag_names = calibre_db.session.query(db_element).filter(db.Tags.id.in_(tags['exclude_' + key])).all()
tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['exclude_' + key])).all()
searchterm.extend(tag.name for tag in tag_names)
language_names = calibre_db.session.query(db.Languages). \
filter(db.Languages.id.in_(tags['include_language'])).all()
@ -1327,7 +1327,11 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
q = q.filter(db.Books.comments.any(func.lower(db.Comments.text).ilike("%" + description + "%")))
# search custom culumns
q = adv_search_custom_columns(cc, term, q)
try:
q = adv_search_custom_columns(cc, term, q)
except AttributeError as ex:
log.debug_or_exception(ex)
flash(_("Error on search for custom columns, please restart Calibre-Web"), category="error")
q = q.order_by(*order).all()
flask_session['query'] = json.dumps(term)
@ -1381,10 +1385,14 @@ def serve_book(book_id, book_format, anyname):
return "File not in Database"
log.info('Serving book: %s', data.name)
if config.config_use_google_drive:
headers = Headers()
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
df = getFileFromEbooksFolder(book.path, data.name + "." + book_format)
return do_gdrive_download(df, headers, (book_format.upper() == 'TXT'))
try:
headers = Headers()
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
df = getFileFromEbooksFolder(book.path, data.name + "." + book_format)
return do_gdrive_download(df, headers, (book_format.upper() == 'TXT'))
except AttributeError as ex:
log.debug_or_exception(ex)
return "File Not Found"
else:
if book_format.upper() == 'TXT':
try:
@ -1394,11 +1402,11 @@ def serve_book(book_id, book_format, anyname):
return make_response(
rawdata.decode(result['encoding']).encode('utf-8'))
except FileNotFoundError:
log.error("File Not Found")
return "File Not Found"
return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format)
@web.route("/download/<int:book_id>/<book_format>", defaults={'anyname': 'None'})
@web.route("/download/<int:book_id>/<book_format>/<anyname>")
@login_required_if_no_ano
@ -1489,9 +1497,9 @@ def register():
@web.route('/login', methods=['GET', 'POST'])
def login():
if not config.db_configured:
log.debug(u"Redirect to initial configuration")
return redirect(url_for('admin.basic_configuration'))
#if not config.db_configured:
# log.debug(u"Redirect to initial configuration")
# return redirect(url_for('admin.basic_configuration'))
if current_user is not None and current_user.is_authenticated:
return redirect(url_for('web.index'))
if config.config_login_type == constants.LOGIN_LDAP and not services.ldap:
@ -1593,6 +1601,8 @@ def change_profile(kobo_support, local_oauth_check, oauth_status, translations,
current_user.default_language = to_save["default_language"]
if to_save.get("locale"):
current_user.locale = to_save["locale"]
current_user.kobo_only_shelves_sync = int(to_save.get("kobo_only_shelves_sync") == "on") or 0
except Exception as ex:
flash(str(ex), category="error")
return render_title_template("user_edit.html", content=current_user,

File diff suppressed because it is too large Load Diff

View File

@ -32,6 +32,7 @@ SQLAlchemy-Utils>=0.33.5,<0.38.0
# extracting metadata
lxml>=3.8.0,<4.7.0
rarfile>=2.7
scholarly>=1.2.0, <1.3
# other
natsort>=2.2.0,<7.2.0

View File

@ -3,7 +3,7 @@ Flask-Babel>=0.11.1,<2.1.0
Flask-Login>=0.3.2,<0.5.1
Flask-Principal>=0.3.2,<0.5.1
backports_abc>=0.4
Flask>=1.0.2,<2.0.0
Flask>=1.0.2,<2.1.0
iso-639>=0.4.5,<0.5.0
PyPDF3>=1.0.0,<1.0.4
pytz>=2016.10

File diff suppressed because it is too large Load Diff