Merge branch 'master' of https://github.com/janeczku/calibre-web
This commit is contained in:
commit
bf4564e365
26
README.md
26
README.md
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
307
cps/admin.py
307
cps/admin.py
|
@ -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"):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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$'
|
||||
|
|
44
cps/db.py
44
cps/db.py
|
@ -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):
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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]:
|
||||
|
|
149
cps/kobo.py
149
cps/kobo.py
|
@ -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
|
||||
|
||||
|
|
48
cps/shelf.py
48
cps/shelf.py
|
@ -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))
|
||||
|
|
204
cps/static/css/libs/viewer.css
vendored
204
cps/static/css/libs/viewer.css
vendored
|
@ -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 {
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
})();
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
|
||||
}
|
||||
|
|
9155
cps/static/js/compress/jszip.js
Normal file
9155
cps/static/js/compress/jszip.js
Normal file
File diff suppressed because it is too large
Load Diff
434
cps/static/js/compress/libunrar.js
Normal file
434
cps/static/js/compress/libunrar.js
Normal file
File diff suppressed because one or more lines are too long
BIN
cps/static/js/compress/libunrar.js.mem
Normal file
BIN
cps/static/js/compress/libunrar.js.mem
Normal file
Binary file not shown.
90
cps/static/js/compress/libuntar.js
Normal file
90
cps/static/js/compress/libuntar.js
Normal 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;
|
||||
})();
|
420
cps/static/js/compress/uncompress.js
Normal file
420
cps/static/js/compress/uncompress.js
Normal 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;
|
||||
})();
|
|
@ -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);
|
||||
});
|
||||
|
|
45
cps/static/js/fullscreen.js
Normal file
45
cps/static/js/fullscreen.js
Normal 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);
|
||||
});
|
|
@ -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);
|
||||
};
|
||||
|
||||
})();
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
|
@ -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;
|
||||
};
|
||||
|
||||
})();
|
|
@ -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 {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
74
cps/templates/config_db.html
Normal file
74
cps/templates/config_db.html
Normal 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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -111,6 +111,7 @@
|
|||
</div>
|
||||
{%endif%}
|
||||
{% endfor %}
|
||||
{% block flash %}{% endblock %}
|
||||
{% if g.current_theme == 1 %}
|
||||
<div id="loader" hidden="true">
|
||||
<center>
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)}}
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
35
cps/ub.py
35
cps/ub.py
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
30
cps/web.py
30
cps/web.py
|
@ -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,
|
||||
|
|
937
messages.pot
937
messages.pot
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue
Block a user