mirror of
https://github.com/janeczku/calibre-web
synced 2024-12-26 18:10:30 +00:00
commit
93720c9fdf
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1,4 +1,4 @@
|
||||
helper.py ident export-subst
|
||||
updater.py ident export-subst
|
||||
/test export-ignore
|
||||
cps/static/css/libs/* linguist-vendored
|
||||
cps/static/js/libs/* linguist-vendored
|
||||
|
@ -1,6 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2016-2019 lemmsh cervinko Kennyl matthazinski OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import uploader
|
||||
import os
|
||||
@ -19,6 +35,7 @@ logger = logging.getLogger("book_formats")
|
||||
try:
|
||||
from wand.image import Image
|
||||
from wand import version as ImageVersion
|
||||
from wand.exceptions import PolicyError
|
||||
use_generic_pdf_cover = False
|
||||
except (ImportError, RuntimeError) as e:
|
||||
logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e)
|
||||
@ -84,7 +101,7 @@ def default_meta(tmp_file_path, original_file_name, original_file_extension):
|
||||
def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
|
||||
|
||||
if use_pdf_meta:
|
||||
pdf = PdfFileReader(open(tmp_file_path, 'rb'))
|
||||
pdf = PdfFileReader(open(tmp_file_path, 'rb'), strict=False)
|
||||
doc_info = pdf.getDocumentInfo()
|
||||
else:
|
||||
doc_info = None
|
||||
@ -114,12 +131,18 @@ def pdf_preview(tmp_file_path, tmp_dir):
|
||||
if use_generic_pdf_cover:
|
||||
return None
|
||||
else:
|
||||
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg"
|
||||
with Image(filename=tmp_file_path + "[0]", resolution=150) as img:
|
||||
img.compression_quality = 88
|
||||
img.save(filename=os.path.join(tmp_dir, cover_file_name))
|
||||
return cover_file_name
|
||||
|
||||
try:
|
||||
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg"
|
||||
with Image(filename=tmp_file_path + "[0]", resolution=150) as img:
|
||||
img.compression_quality = 88
|
||||
img.save(filename=os.path.join(tmp_dir, cover_file_name))
|
||||
return cover_file_name
|
||||
except PolicyError as ex:
|
||||
logger.warning('Pdf extraction forbidden by Imagemagick policy: %s', ex)
|
||||
return None
|
||||
except Exception as ex:
|
||||
logger.warning('Cannot extract cover image, using default: %s', ex)
|
||||
return None
|
||||
|
||||
def get_versions():
|
||||
if not use_generic_pdf_cover:
|
||||
|
@ -1,3 +1,19 @@
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2016-2019 jkrehm andy29485 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/>.
|
||||
|
||||
# Inspired by https://github.com/ChrisTM/Flask-CacheBust
|
||||
# Uses query strings so CSS font files are found without having to resort to absolute URLs
|
||||
|
||||
|
17
cps/cli.py
17
cps/cli.py
@ -1,6 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2018 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
47
cps/comic.py
47
cps/comic.py
@ -1,6 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2018 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import zipfile
|
||||
import tarfile
|
||||
import os
|
||||
@ -8,21 +24,34 @@ import uploader
|
||||
|
||||
|
||||
def extractCover(tmp_file_name, original_file_extension):
|
||||
cover_data = None
|
||||
if original_file_extension.upper() == '.CBZ':
|
||||
cf = zipfile.ZipFile(tmp_file_name)
|
||||
compressed_name = cf.namelist()[0]
|
||||
cover_data = cf.read(compressed_name)
|
||||
for name in cf.namelist():
|
||||
ext = os.path.splitext(name)
|
||||
if len(ext) > 1:
|
||||
extension = ext[1].lower()
|
||||
if extension == '.jpg':
|
||||
cover_data = cf.read(name)
|
||||
break
|
||||
elif original_file_extension.upper() == '.CBT':
|
||||
cf = tarfile.TarFile(tmp_file_name)
|
||||
compressed_name = cf.getnames()[0]
|
||||
cover_data = cf.extractfile(compressed_name).read()
|
||||
for name in cf.getnames():
|
||||
ext = os.path.splitext(name)
|
||||
if len(ext) > 1:
|
||||
extension = ext[1].lower()
|
||||
if extension == '.jpg':
|
||||
cover_data = cf.extractfile(name).read()
|
||||
break
|
||||
|
||||
prefix = os.path.dirname(tmp_file_name)
|
||||
|
||||
tmp_cover_name = prefix + '/cover' + os.path.splitext(compressed_name)[1]
|
||||
image = open(tmp_cover_name, 'wb')
|
||||
image.write(cover_data)
|
||||
image.close()
|
||||
if cover_data:
|
||||
tmp_cover_name = prefix + '/cover' + extension
|
||||
image = open(tmp_cover_name, 'wb')
|
||||
image.write(cover_data)
|
||||
image.close()
|
||||
else:
|
||||
tmp_cover_name = None
|
||||
return tmp_cover_name
|
||||
|
||||
|
||||
|
@ -1,5 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2016-2019 Ben Bennett, OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import ub
|
||||
|
20
cps/db.py
20
cps/db.py
@ -1,6 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2012-2019 mutschler, cervinko, ok11, jkrehm, nanu-c, Wineliva,
|
||||
# pjeby, elelay, idalin, 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/>.
|
||||
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import *
|
||||
@ -9,6 +26,7 @@ import re
|
||||
import ast
|
||||
from ub import config
|
||||
import ub
|
||||
import sys
|
||||
|
||||
session = None
|
||||
cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series']
|
||||
@ -301,6 +319,8 @@ class Custom_Columns(Base):
|
||||
|
||||
def get_display_dict(self):
|
||||
display_dict = ast.literal_eval(self.display)
|
||||
if sys.version_info < (3, 0):
|
||||
display_dict['enum_values'] = [x.decode('unicode_escape') for x in display_dict['enum_values']]
|
||||
return display_dict
|
||||
|
||||
|
||||
|
16
cps/epub.py
16
cps/epub.py
@ -1,6 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2018 lemmsh, Kennyl, Kyosfonica, matthazinski
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import zipfile
|
||||
from lxml import etree
|
||||
import os
|
||||
|
16
cps/fb2.py
16
cps/fb2.py
@ -1,6 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2018 lemmsh, cervinko, 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/>.
|
||||
|
||||
from lxml import etree
|
||||
import uploader
|
||||
|
||||
|
@ -1,7 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2018 idalin, 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/>.
|
||||
|
||||
try:
|
||||
from pydrive.auth import GoogleAuth
|
||||
from pydrive.drive import GoogleDrive
|
||||
from pydrive.auth import RefreshError
|
||||
from pydrive.auth import RefreshError, InvalidConfigError
|
||||
from apiclient import errors
|
||||
gdrive_support = True
|
||||
except ImportError:
|
||||
@ -12,12 +31,9 @@ from ub import config
|
||||
import cli
|
||||
import shutil
|
||||
from flask import Response, stream_with_context
|
||||
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import *
|
||||
|
||||
|
||||
import web
|
||||
|
||||
class Singleton:
|
||||
@ -112,7 +128,8 @@ def migrate():
|
||||
sql=sql[0].replace(currUniqueConstraint, 'UNIQUE (gdrive_id, path)')
|
||||
sql=sql.replace(GdriveId.__tablename__, GdriveId.__tablename__ + '2')
|
||||
session.execute(sql)
|
||||
session.execute('INSERT INTO gdrive_ids2 (id, gdrive_id, path) SELECT id, gdrive_id, path FROM gdrive_ids;')
|
||||
session.execute("INSERT INTO gdrive_ids2 (id, gdrive_id, path) SELECT id, "
|
||||
"gdrive_id, path FROM gdrive_ids;")
|
||||
session.commit()
|
||||
session.execute('DROP TABLE %s' % 'gdrive_ids')
|
||||
session.execute('ALTER TABLE gdrive_ids2 RENAME to gdrive_ids')
|
||||
@ -146,7 +163,10 @@ def getDrive(drive=None, gauth=None):
|
||||
# Save the current credentials to a file
|
||||
return GoogleDrive(gauth)
|
||||
if drive.auth.access_token_expired:
|
||||
drive.auth.Refresh()
|
||||
try:
|
||||
drive.auth.Refresh()
|
||||
except RefreshError as e:
|
||||
web.app.logger.error("Google Drive error: " + e.message)
|
||||
return drive
|
||||
|
||||
def listRootFolders():
|
||||
@ -164,8 +184,9 @@ def getFolderInFolder(parentId, folderName, drive):
|
||||
# drive = getDrive(drive)
|
||||
query=""
|
||||
if folderName:
|
||||
query = "title = '%s' and " % folderName.replace("'", "\\'")
|
||||
folder = query + "'%s' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false" % parentId
|
||||
query = "title = '%s' and " % folderName.replace("'", r"\'")
|
||||
folder = query + "'%s' in parents and mimeType = 'application/vnd.google-apps.folder'" \
|
||||
" and trashed = false" % parentId
|
||||
fileList = drive.ListFile({'q': folder}).GetList()
|
||||
if fileList.__len__() == 0:
|
||||
return None
|
||||
@ -190,8 +211,7 @@ def getEbooksFolderId(drive=None):
|
||||
|
||||
|
||||
def getFile(pathId, fileName, drive):
|
||||
metaDataFile = "'%s' in parents and trashed = false and title = '%s'" % (pathId, fileName.replace("'", "\\'"))
|
||||
|
||||
metaDataFile = "'%s' in parents and trashed = false and title = '%s'" % (pathId, fileName.replace("'", r"\'"))
|
||||
fileList = drive.ListFile({'q': metaDataFile}).GetList()
|
||||
if fileList.__len__() == 0:
|
||||
return None
|
||||
@ -226,7 +246,7 @@ def getFolderId(path, drive):
|
||||
dbChange = True
|
||||
currentFolderId = currentFolder['id']
|
||||
else:
|
||||
currentFolderId= None
|
||||
currentFolderId = None
|
||||
break
|
||||
if dbChange:
|
||||
session.commit()
|
||||
@ -248,16 +268,9 @@ def getFileFromEbooksFolder(path, fileName):
|
||||
return None
|
||||
|
||||
|
||||
'''def copyDriveFileRemote(drive, origin_file_id, copy_title):
|
||||
drive = getDrive(drive)
|
||||
copied_file = {'title': copy_title}
|
||||
try:
|
||||
file_data = drive.auth.service.files().copy(
|
||||
fileId = origin_file_id, body=copied_file).execute()
|
||||
return drive.CreateFile({'id': file_data['id']})
|
||||
except errors.HttpError as error:
|
||||
print ('An error occurred: %s' % error)
|
||||
return None'''
|
||||
def moveGdriveFileRemote(origin_file_id, new_title):
|
||||
origin_file_id['title']= new_title
|
||||
origin_file_id.Upload()
|
||||
|
||||
|
||||
# Download metadata.db from gdrive
|
||||
@ -269,9 +282,10 @@ def downloadFile(path, filename, output):
|
||||
def moveGdriveFolderRemote(origin_file, target_folder):
|
||||
drive = getDrive(Gdrive.Instance().drive)
|
||||
previous_parents = ",".join([parent["id"] for parent in origin_file.get('parents')])
|
||||
children = drive.auth.service.children().list(folderId=previous_parents).execute()
|
||||
gFileTargetDir = getFileFromEbooksFolder(None, target_folder)
|
||||
if not gFileTargetDir:
|
||||
# Folder is not exisiting, create, and move folder
|
||||
# Folder is not existing, create, and move folder
|
||||
gFileTargetDir = drive.CreateFile(
|
||||
{'title': target_folder, 'parents': [{"kind": "drive#fileLink", 'id': getEbooksFolderId()}],
|
||||
"mimeType": "application/vnd.google-apps.folder"})
|
||||
@ -281,13 +295,10 @@ def moveGdriveFolderRemote(origin_file, target_folder):
|
||||
addParents=gFileTargetDir['id'],
|
||||
removeParents=previous_parents,
|
||||
fields='id, parents').execute()
|
||||
# if previous_parents has no childs anymore, delete originfileparent
|
||||
# is not working correctly, because of slow update on gdrive -> could cause trouble in gdrive.db
|
||||
# (nonexisting folder has id)
|
||||
# children = drive.auth.service.children().list(folderId=previous_parents).execute()
|
||||
# if not len(children['items']):
|
||||
# drive.auth.service.files().delete(fileId=previous_parents).execute()
|
||||
|
||||
# if previous_parents has no childs anymore, delete original fileparent
|
||||
if len(children['items']) == 1:
|
||||
deleteDatabaseEntry(previous_parents)
|
||||
drive.auth.service.files().delete(fileId=previous_parents).execute()
|
||||
|
||||
|
||||
def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
|
||||
@ -299,9 +310,11 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
|
||||
if not parent:
|
||||
parent = getEbooksFolder(drive)
|
||||
if os.path.isdir(os.path.join(prevDir,uploadFile)):
|
||||
existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % (os.path.basename(uploadFile), parent['id'])}).GetList()
|
||||
existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
|
||||
(os.path.basename(uploadFile).replace("'", r"\'"), parent['id'])}).GetList()
|
||||
if len(existingFolder) == 0 and (not isInitial or createRoot):
|
||||
parent = drive.CreateFile({'title': os.path.basename(uploadFile), 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],
|
||||
parent = drive.CreateFile({'title': os.path.basename(uploadFile),
|
||||
'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],
|
||||
"mimeType": "application/vnd.google-apps.folder"})
|
||||
parent.Upload()
|
||||
else:
|
||||
@ -312,11 +325,13 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
|
||||
copyToDrive(drive, f, True, replaceFiles, ignoreFiles, parent, os.path.join(prevDir, uploadFile))
|
||||
else:
|
||||
if os.path.basename(uploadFile) not in ignoreFiles:
|
||||
existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % (os.path.basename(uploadFile), parent['id'])}).GetList()
|
||||
existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
|
||||
(os.path.basename(uploadFile).replace("'", r"\'"), parent['id'])}).GetList()
|
||||
if len(existingFiles) > 0:
|
||||
driveFile = existingFiles[0]
|
||||
else:
|
||||
driveFile = drive.CreateFile({'title': os.path.basename(uploadFile), 'parents': [{"kind":"drive#fileLink", 'id': parent['id']}], })
|
||||
driveFile = drive.CreateFile({'title': os.path.basename(uploadFile).replace("'", r"\'"),
|
||||
'parents': [{"kind":"drive#fileLink", 'id': parent['id']}], })
|
||||
driveFile.SetContentFile(os.path.join(prevDir, uploadFile))
|
||||
driveFile.Upload()
|
||||
|
||||
@ -327,7 +342,8 @@ def uploadFileToEbooksFolder(destFile, f):
|
||||
splitDir = destFile.split('/')
|
||||
for i, x in enumerate(splitDir):
|
||||
if i == len(splitDir)-1:
|
||||
existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % (x, parent['id'])}).GetList()
|
||||
existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
|
||||
(x.replace("'", r"\'"), parent['id'])}).GetList()
|
||||
if len(existingFiles) > 0:
|
||||
driveFile = existingFiles[0]
|
||||
else:
|
||||
@ -335,7 +351,8 @@ def uploadFileToEbooksFolder(destFile, f):
|
||||
driveFile.SetContentFile(f)
|
||||
driveFile.Upload()
|
||||
else:
|
||||
existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % (x, parent['id'])}).GetList()
|
||||
existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
|
||||
(x.replace("'", r"\'"), parent['id'])}).GetList()
|
||||
if len(existingFolder) == 0:
|
||||
parent = drive.CreateFile({'title': x, 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],
|
||||
"mimeType": "application/vnd.google-apps.folder"})
|
||||
@ -428,6 +445,10 @@ def getChangeById (drive, change_id):
|
||||
except (errors.HttpError) as error:
|
||||
web.app.logger.info(error.message)
|
||||
return None
|
||||
except Exception as e:
|
||||
web.app.logger.info(e)
|
||||
return None
|
||||
|
||||
|
||||
# Deletes the local hashes database to force search for new folder names
|
||||
def deleteDatabaseOnChange():
|
||||
@ -442,9 +463,10 @@ def updateGdriveCalibreFromLocal():
|
||||
|
||||
# update gdrive.db on edit of books title
|
||||
def updateDatabaseOnEdit(ID,newPath):
|
||||
sqlCheckPath = newPath if newPath[-1] == '/' else newPath + u'/'
|
||||
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
|
||||
if storedPathName:
|
||||
storedPathName.path = newPath
|
||||
storedPathName.path = sqlCheckPath
|
||||
session.commit()
|
||||
|
||||
|
||||
|
463
cps/helper.py
Executable file → Normal file
463
cps/helper.py
Executable file → Normal file
@ -1,31 +1,46 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2012-2019 cervinko, idalin, SiphonSquirrel, ouzklcn, akushsky,
|
||||
# OzzieIsaacs, bodybybuddha, jkrehm, matthazinski, janeczku
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import db
|
||||
import ub
|
||||
from flask import current_app as app
|
||||
import logging
|
||||
from tempfile import gettempdir
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
from io import BytesIO
|
||||
import worker
|
||||
import time
|
||||
|
||||
from flask import send_from_directory, make_response, redirect, abort
|
||||
from flask_babel import gettext as _
|
||||
import threading
|
||||
from flask_login import current_user
|
||||
from babel.dates import format_datetime
|
||||
from datetime import datetime
|
||||
import shutil
|
||||
import requests
|
||||
import zipfile
|
||||
try:
|
||||
import gdriveutils as gd
|
||||
except ImportError:
|
||||
pass
|
||||
import web
|
||||
import server
|
||||
import random
|
||||
import subprocess
|
||||
|
||||
@ -36,7 +51,7 @@ except ImportError:
|
||||
use_unidecode = False
|
||||
|
||||
# Global variables
|
||||
updater_thread = None
|
||||
# updater_thread = None
|
||||
global_WorkerThread = worker.WorkerThread()
|
||||
global_WorkerThread.start()
|
||||
|
||||
@ -73,10 +88,12 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
|
||||
# read settings and append converter task to queue
|
||||
if kindle_mail:
|
||||
settings = ub.get_mail_settings()
|
||||
text = _(u"Convert: %(book)s" , book=book.title)
|
||||
settings['subject'] = _('Send to Kindle') # pretranslate Subject for e-mail
|
||||
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
|
||||
# text = _(u"%(format)s: %(book)s", format=new_book_format, book=book.title)
|
||||
else:
|
||||
settings = dict()
|
||||
text = _(u"Convert to %(format)s: %(book)s", format=new_book_format, book=book.title)
|
||||
text = (u"%s -> %s: %s" % (old_book_format, new_book_format, book.title))
|
||||
settings['old_book_format'] = old_book_format
|
||||
settings['new_book_format'] = new_book_format
|
||||
global_WorkerThread.add_convert(file_path, book.id, user_id, text, settings, kindle_mail)
|
||||
@ -89,7 +106,8 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
|
||||
|
||||
def send_test_mail(kindle_mail, user_name):
|
||||
global_WorkerThread.add_email(_(u'Calibre-Web test e-mail'),None, None, ub.get_mail_settings(),
|
||||
kindle_mail, user_name, _(u"Test e-mail"))
|
||||
kindle_mail, user_name, _(u"Test e-mail"),
|
||||
_(u'This e-mail has been sent via Calibre-Web.'))
|
||||
return
|
||||
|
||||
|
||||
@ -105,44 +123,83 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
|
||||
text += "Sincerely\r\n\r\n"
|
||||
text += "Your Calibre-Web team"
|
||||
global_WorkerThread.add_email(_(u'Get Started with Calibre-Web'),None, None, ub.get_mail_settings(),
|
||||
e_mail, user_name, _(u"Registration e-mail for user: %(name)s", name=user_name),text)
|
||||
e_mail, user_name, _(u"Registration e-mail for user: %(name)s", name=user_name), text)
|
||||
return
|
||||
|
||||
def check_send_to_kindle(entry):
|
||||
"""
|
||||
returns all available book formats for sending to Kindle
|
||||
"""
|
||||
if len(entry.data):
|
||||
bookformats=list()
|
||||
if ub.config.config_ebookconverter == 0:
|
||||
# no converter - only for mobi and pdf formats
|
||||
for ele in iter(entry.data):
|
||||
if 'MOBI' in ele.format:
|
||||
bookformats.append({'format':'Mobi','convert':0,'text':_('Send %(format)s to Kindle',format='Mobi')})
|
||||
if 'PDF' in ele.format:
|
||||
bookformats.append({'format':'Pdf','convert':0,'text':_('Send %(format)s to Kindle',format='Pdf')})
|
||||
if 'AZW' in ele.format:
|
||||
bookformats.append({'format':'Azw','convert':0,'text':_('Send %(format)s to Kindle',format='Azw')})
|
||||
if 'AZW3' in ele.format:
|
||||
bookformats.append({'format':'Azw3','convert':0,'text':_('Send %(format)s to Kindle',format='Azw3')})
|
||||
else:
|
||||
formats = list()
|
||||
for ele in iter(entry.data):
|
||||
formats.append(ele.format)
|
||||
if 'MOBI' in formats:
|
||||
bookformats.append({'format': 'Mobi','convert':0,'text':_('Send %(format)s to Kindle',format='Mobi')})
|
||||
if 'AZW' in formats:
|
||||
bookformats.append({'format': 'Azw','convert':0,'text':_('Send %(format)s to Kindle',format='Azw')})
|
||||
if 'AZW3' in formats:
|
||||
bookformats.append({'format': 'Azw3','convert':0,'text':_('Send %(format)s to Kindle',format='Azw3')})
|
||||
if 'PDF' in formats:
|
||||
bookformats.append({'format': 'Pdf','convert':0,'text':_('Send %(format)s to Kindle',format='Pdf')})
|
||||
if ub.config.config_ebookconverter >= 1:
|
||||
if 'EPUB' in formats and not 'MOBI' in formats:
|
||||
bookformats.append({'format': 'Mobi','convert':1,
|
||||
'text':_('Convert %(orig)s to %(format)s and send to Kindle',orig='Epub',format='Mobi')})
|
||||
if ub.config.config_ebookconverter == 2:
|
||||
if 'EPUB' in formats and not 'AZW3' in formats:
|
||||
bookformats.append({'format': 'Azw3','convert':1,
|
||||
'text':_('Convert %(orig)s to %(format)s and send to Kindle',orig='Epub',format='Azw3')})
|
||||
return bookformats
|
||||
else:
|
||||
app.logger.error(u'Cannot find book entry %d', entry.id)
|
||||
return None
|
||||
|
||||
|
||||
# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return
|
||||
# list with supported formats
|
||||
def check_read_formats(entry):
|
||||
EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'ZIP', 'CBZ', 'TAR', 'CBT', 'RAR', 'CBR'}
|
||||
bookformats = list()
|
||||
if len(entry.data):
|
||||
for ele in iter(entry.data):
|
||||
if ele.format in EXTENSIONS_READER:
|
||||
bookformats.append(ele.format.lower())
|
||||
return bookformats
|
||||
|
||||
|
||||
# Files are processed in the following order/priority:
|
||||
# 1: If Mobi file is exisiting, it's directly send to kindle email,
|
||||
# 2: If Epub file is exisiting, it's converted and send to kindle email
|
||||
# 3: If Pdf file is exisiting, it's directly send to kindle email,
|
||||
def send_mail(book_id, kindle_mail, calibrepath, user_id):
|
||||
# 1: If Mobi file is existing, it's directly send to kindle email,
|
||||
# 2: If Epub file is existing, it's converted and send to kindle email,
|
||||
# 3: If Pdf file is existing, it's directly send to kindle email
|
||||
def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
|
||||
"""Send email with attachments"""
|
||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
||||
data = db.session.query(db.Data).filter(db.Data.book == book.id).all()
|
||||
|
||||
formats = {}
|
||||
for entry in data:
|
||||
if entry.format == "MOBI":
|
||||
formats["mobi"] = entry.name + ".mobi"
|
||||
if entry.format == "EPUB":
|
||||
formats["epub"] = entry.name + ".epub"
|
||||
if entry.format == "PDF":
|
||||
formats["pdf"] = entry.name + ".pdf"
|
||||
|
||||
if len(formats) == 0:
|
||||
return _(u"Could not find any formats suitable for sending by e-mail")
|
||||
|
||||
if 'mobi' in formats:
|
||||
result = formats['mobi']
|
||||
elif 'epub' in formats:
|
||||
# returns None if sucess, otherwise errormessage
|
||||
return convert_book_format(book_id, calibrepath, u'epub', u'mobi', user_id, kindle_mail)
|
||||
elif 'pdf' in formats:
|
||||
result = formats['pdf'] # worker.get_attachment()
|
||||
else:
|
||||
return _(u"Could not find any formats suitable for sending by e-mail")
|
||||
if result:
|
||||
global_WorkerThread.add_email(_(u"Send to Kindle"), book.path, result, ub.get_mail_settings(),
|
||||
kindle_mail, user_id, _(u"E-mail: %(book)s", book=book.title))
|
||||
if convert:
|
||||
# returns None if success, otherwise errormessage
|
||||
return convert_book_format(book_id, calibrepath, u'epub', book_format.lower(), user_id, kindle_mail)
|
||||
else:
|
||||
for entry in iter(book.data):
|
||||
if entry.format.upper() == book_format.upper():
|
||||
result = entry.name + '.' + book_format.lower()
|
||||
global_WorkerThread.add_email(_(u"Send to Kindle"), book.path, result, ub.get_mail_settings(),
|
||||
kindle_mail, user_id, _(u"E-mail: %(book)s", book=book.title),
|
||||
_(u'This e-mail has been sent via Calibre-Web.'))
|
||||
return
|
||||
return _(u"The requested file could not be read. Maybe wrong permissions?")
|
||||
|
||||
|
||||
@ -173,18 +230,26 @@ def get_valid_filename(value, replace_whitespace=True):
|
||||
value = value[:128]
|
||||
if not value:
|
||||
raise ValueError("Filename cannot be empty")
|
||||
return value
|
||||
if sys.version_info.major == 3:
|
||||
return value
|
||||
else:
|
||||
return value.decode('utf-8')
|
||||
|
||||
|
||||
def get_sorted_author(value):
|
||||
try:
|
||||
regexes = ["^(JR|SR)\.?$", "^I{1,3}\.?$", "^IV\.?$"]
|
||||
combined = "(" + ")|(".join(regexes) + ")"
|
||||
value = value.split(" ")
|
||||
if re.match(combined, value[-1].upper()):
|
||||
value2 = value[-2] + ", " + " ".join(value[:-2]) + " " + value[-1]
|
||||
if ',' not in value:
|
||||
regexes = ["^(JR|SR)\.?$", "^I{1,3}\.?$", "^IV\.?$"]
|
||||
combined = "(" + ")|(".join(regexes) + ")"
|
||||
value = value.split(" ")
|
||||
if re.match(combined, value[-1].upper()):
|
||||
value2 = value[-2] + ", " + " ".join(value[:-2]) + " " + value[-1]
|
||||
elif len(value) == 1:
|
||||
value2 = value[0]
|
||||
else:
|
||||
value2 = value[-1] + ", " + " ".join(value[:-1])
|
||||
else:
|
||||
value2 = value[-1] + ", " + " ".join(value[:-1])
|
||||
value2 = value
|
||||
except Exception:
|
||||
web.app.logger.error("Sorting author " + str(value) + "failed")
|
||||
value2 = value
|
||||
@ -213,12 +278,15 @@ def delete_book_file(book, calibrepath, book_format=None):
|
||||
return False
|
||||
|
||||
|
||||
def update_dir_structure_file(book_id, calibrepath):
|
||||
def update_dir_structure_file(book_id, calibrepath, first_author):
|
||||
localbook = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
||||
path = os.path.join(calibrepath, localbook.path)
|
||||
|
||||
authordir = localbook.path.split('/')[0]
|
||||
new_authordir = get_valid_filename(localbook.authors[0].name)
|
||||
if first_author:
|
||||
new_authordir = get_valid_filename(first_author)
|
||||
else:
|
||||
new_authordir = get_valid_filename(localbook.authors[0].name)
|
||||
|
||||
titledir = localbook.path.split('/')[1]
|
||||
new_titledir = get_valid_filename(localbook.title) + " (" + str(book_id) + ")"
|
||||
@ -232,53 +300,86 @@ def update_dir_structure_file(book_id, calibrepath):
|
||||
web.app.logger.info("Copying title: " + path + " into existing: " + new_title_path)
|
||||
for dir_name, subdir_list, file_list in os.walk(path):
|
||||
for file in file_list:
|
||||
os.renames(os.path.join(dir_name, file), os.path.join(new_title_path + dir_name[len(path):], file))
|
||||
os.renames(os.path.join(dir_name, file),
|
||||
os.path.join(new_title_path + dir_name[len(path):], file))
|
||||
path = new_title_path
|
||||
localbook.path = localbook.path.split('/')[0] + '/' + new_titledir
|
||||
except OSError as ex:
|
||||
web.app.logger.error("Rename title from: " + path + " to " + new_title_path + ": " + str(ex))
|
||||
web.app.logger.debug(ex, exc_info=True)
|
||||
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_title_path, error=str(ex))
|
||||
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
||||
src=path, dest=new_title_path, error=str(ex))
|
||||
if authordir != new_authordir:
|
||||
try:
|
||||
new_author_path = os.path.join(os.path.join(calibrepath, new_authordir), os.path.basename(path))
|
||||
new_author_path = os.path.join(calibrepath, new_authordir, os.path.basename(path))
|
||||
os.renames(path, new_author_path)
|
||||
localbook.path = new_authordir + '/' + localbook.path.split('/')[1]
|
||||
except OSError as ex:
|
||||
web.app.logger.error("Rename author from: " + path + " to " + new_author_path + ": " + str(ex))
|
||||
web.app.logger.debug(ex, exc_info=True)
|
||||
return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_author_path, error=str(ex))
|
||||
return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
||||
src=path, dest=new_author_path, error=str(ex))
|
||||
# Rename all files from old names to new names
|
||||
if authordir != new_authordir or titledir != new_titledir:
|
||||
try:
|
||||
new_name = get_valid_filename(localbook.title) + ' - ' + get_valid_filename(new_authordir)
|
||||
path_name = os.path.join(calibrepath, new_authordir, os.path.basename(path))
|
||||
for file_format in localbook.data:
|
||||
os.renames(os.path.join(path_name, file_format.name + '.' + file_format.format.lower()),
|
||||
os.path.join(path_name, new_name + '.' + file_format.format.lower()))
|
||||
file_format.name = new_name
|
||||
except OSError as ex:
|
||||
web.app.logger.error("Rename file in path " + path + " to " + new_name + ": " + str(ex))
|
||||
web.app.logger.debug(ex, exc_info=True)
|
||||
return _("Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
||||
src=path, dest=new_name, error=str(ex))
|
||||
return False
|
||||
|
||||
|
||||
def update_dir_structure_gdrive(book_id):
|
||||
def update_dir_structure_gdrive(book_id, first_author):
|
||||
error = False
|
||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
||||
path = book.path
|
||||
|
||||
authordir = book.path.split('/')[0]
|
||||
new_authordir = get_valid_filename(book.authors[0].name)
|
||||
if first_author:
|
||||
new_authordir = get_valid_filename(first_author)
|
||||
else:
|
||||
new_authordir = get_valid_filename(book.authors[0].name)
|
||||
titledir = book.path.split('/')[1]
|
||||
new_titledir = get_valid_filename(book.title) + " (" + str(book_id) + ")"
|
||||
new_titledir = get_valid_filename(book.title) + u" (" + str(book_id) + u")"
|
||||
|
||||
if titledir != new_titledir:
|
||||
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
|
||||
if gFile:
|
||||
gFile['title'] = new_titledir
|
||||
|
||||
gFile.Upload()
|
||||
book.path = book.path.split('/')[0] + '/' + new_titledir
|
||||
book.path = book.path.split('/')[0] + u'/' + new_titledir
|
||||
path = book.path
|
||||
gd.updateDatabaseOnEdit(gFile['id'], book.path) # only child folder affected
|
||||
else:
|
||||
error = _(u'File %(file)s not found on Google Drive', file= book.path) # file not found
|
||||
error = _(u'File %(file)s not found on Google Drive', file=book.path) # file not found
|
||||
|
||||
if authordir != new_authordir:
|
||||
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
|
||||
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), new_titledir)
|
||||
if gFile:
|
||||
gd.moveGdriveFolderRemote(gFile,new_authordir)
|
||||
book.path = new_authordir + '/' + book.path.split('/')[1]
|
||||
gd.moveGdriveFolderRemote(gFile, new_authordir)
|
||||
book.path = new_authordir + u'/' + book.path.split('/')[1]
|
||||
path = book.path
|
||||
gd.updateDatabaseOnEdit(gFile['id'], book.path)
|
||||
else:
|
||||
error = _(u'File %(file)s not found on Google Drive', file=authordir) # file not found
|
||||
# Rename all files from old names to new names
|
||||
|
||||
if authordir != new_authordir or titledir != new_titledir:
|
||||
new_name = get_valid_filename(book.title) + u' - ' + get_valid_filename(new_authordir)
|
||||
for file_format in book.data:
|
||||
gFile = gd.getFileFromEbooksFolder(path, file_format.name + u'.' + file_format.format.lower())
|
||||
if not gFile:
|
||||
error = _(u'File %(file)s not found on Google Drive', file=file_format.name) # file not found
|
||||
break
|
||||
gd.moveGdriveFileRemote(gFile, new_name + u'.' + file_format.format.lower())
|
||||
file_format.name = new_name
|
||||
return error
|
||||
|
||||
|
||||
@ -299,6 +400,7 @@ def delete_book_gdrive(book, book_format):
|
||||
error =_(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found
|
||||
return error
|
||||
|
||||
|
||||
def generate_random_password():
|
||||
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
|
||||
passlen = 8
|
||||
@ -306,11 +408,12 @@ def generate_random_password():
|
||||
|
||||
################################## External interface
|
||||
|
||||
def update_dir_stucture(book_id, calibrepath):
|
||||
def update_dir_stucture(book_id, calibrepath, first_author = None):
|
||||
if ub.config.config_use_google_drive:
|
||||
return update_dir_structure_gdrive(book_id)
|
||||
return update_dir_structure_gdrive(book_id, first_author)
|
||||
else:
|
||||
return update_dir_structure_file(book_id, calibrepath)
|
||||
return update_dir_structure_file(book_id, calibrepath, first_author)
|
||||
|
||||
|
||||
def delete_book(book, calibrepath, book_format):
|
||||
if ub.config.config_use_google_drive:
|
||||
@ -318,9 +421,12 @@ def delete_book(book, calibrepath, book_format):
|
||||
else:
|
||||
return delete_book_file(book, calibrepath, book_format)
|
||||
|
||||
|
||||
def get_book_cover(cover_path):
|
||||
if ub.config.config_use_google_drive:
|
||||
try:
|
||||
if not web.is_gdrive_ready():
|
||||
return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg")
|
||||
path=gd.get_cover_via_gdrive(cover_path)
|
||||
if path:
|
||||
return redirect(path)
|
||||
@ -328,13 +434,14 @@ def get_book_cover(cover_path):
|
||||
web.app.logger.error(cover_path + '/cover.jpg not found on Google Drive')
|
||||
return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg")
|
||||
except Exception as e:
|
||||
web.app.logger.error("Error Message: "+e.message)
|
||||
web.app.logger.error("Error Message: " + e.message)
|
||||
web.app.logger.exception(e)
|
||||
# traceback.print_exc()
|
||||
return send_from_directory(os.path.join(os.path.dirname(__file__), "static"),"generic_cover.jpg")
|
||||
else:
|
||||
return send_from_directory(os.path.join(ub.config.config_calibre_dir, cover_path), "cover.jpg")
|
||||
|
||||
|
||||
# saves book cover to gdrive or locally
|
||||
def save_cover(url, book_path):
|
||||
img = requests.get(url)
|
||||
@ -347,7 +454,7 @@ def save_cover(url, book_path):
|
||||
f = open(os.path.join(tmpDir, "uploaded_cover.jpg"), "wb")
|
||||
f.write(img.content)
|
||||
f.close()
|
||||
uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), os.path.join(tmpDir, f.name))
|
||||
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), os.path.join(tmpDir, f.name))
|
||||
web.app.logger.info("Cover is saved on Google Drive")
|
||||
return True
|
||||
|
||||
@ -357,6 +464,7 @@ def save_cover(url, book_path):
|
||||
web.app.logger.info("Cover is saved")
|
||||
return True
|
||||
|
||||
|
||||
def do_download_file(book, book_format, data, headers):
|
||||
if ub.config.config_use_google_drive:
|
||||
startTime = time.time()
|
||||
@ -367,163 +475,19 @@ def do_download_file(book, book_format, data, headers):
|
||||
else:
|
||||
abort(404)
|
||||
else:
|
||||
response = make_response(send_from_directory(os.path.join(ub.config.config_calibre_dir, book.path), data.name + "." + book_format))
|
||||
filename = os.path.join(ub.config.config_calibre_dir, book.path)
|
||||
if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)):
|
||||
# ToDo: improve error handling
|
||||
web.app.logger.error('File not found: %s' % os.path.join(filename, data.name + "." + book_format))
|
||||
response = make_response(send_from_directory(filename, data.name + "." + book_format))
|
||||
response.headers = headers
|
||||
return response
|
||||
|
||||
##################################
|
||||
|
||||
|
||||
class Updater(threading.Thread):
|
||||
|
||||
def __init__(self):
|
||||
threading.Thread.__init__(self)
|
||||
self.status = 0
|
||||
|
||||
def run(self):
|
||||
self.status = 1
|
||||
r = requests.get('https://api.github.com/repos/janeczku/calibre-web/zipball/master', stream=True)
|
||||
fname = re.findall("filename=(.+)", r.headers['content-disposition'])[0]
|
||||
self.status = 2
|
||||
z = zipfile.ZipFile(BytesIO(r.content))
|
||||
self.status = 3
|
||||
tmp_dir = gettempdir()
|
||||
z.extractall(tmp_dir)
|
||||
self.status = 4
|
||||
self.update_source(os.path.join(tmp_dir, os.path.splitext(fname)[0]), ub.config.get_main_dir)
|
||||
self.status = 5
|
||||
db.session.close()
|
||||
db.engine.dispose()
|
||||
ub.session.close()
|
||||
ub.engine.dispose()
|
||||
self.status = 6
|
||||
server.Server.setRestartTyp(True)
|
||||
server.Server.stopServer()
|
||||
self.status = 7
|
||||
|
||||
def get_update_status(self):
|
||||
return self.status
|
||||
|
||||
@classmethod
|
||||
def file_to_list(self, filelist):
|
||||
return [x.strip() for x in open(filelist, 'r') if not x.startswith('#EXT')]
|
||||
|
||||
@classmethod
|
||||
def one_minus_two(self, one, two):
|
||||
return [x for x in one if x not in set(two)]
|
||||
|
||||
@classmethod
|
||||
def reduce_dirs(self, delete_files, new_list):
|
||||
new_delete = []
|
||||
for filename in delete_files:
|
||||
parts = filename.split(os.sep)
|
||||
sub = ''
|
||||
for part in parts:
|
||||
sub = os.path.join(sub, part)
|
||||
if sub == '':
|
||||
sub = os.sep
|
||||
count = 0
|
||||
for song in new_list:
|
||||
if song.startswith(sub):
|
||||
count += 1
|
||||
break
|
||||
if count == 0:
|
||||
if sub != '\\':
|
||||
new_delete.append(sub)
|
||||
break
|
||||
return list(set(new_delete))
|
||||
|
||||
@classmethod
|
||||
def reduce_files(self, remove_items, exclude_items):
|
||||
rf = []
|
||||
for item in remove_items:
|
||||
if not item.startswith(exclude_items):
|
||||
rf.append(item)
|
||||
return rf
|
||||
|
||||
@classmethod
|
||||
def moveallfiles(self, root_src_dir, root_dst_dir):
|
||||
change_permissions = True
|
||||
if sys.platform == "win32" or sys.platform == "darwin":
|
||||
change_permissions = False
|
||||
else:
|
||||
logging.getLogger('cps.web').debug('Update on OS-System : ' + sys.platform)
|
||||
new_permissions = os.stat(root_dst_dir)
|
||||
# print new_permissions
|
||||
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):
|
||||
os.makedirs(dst_dir)
|
||||
logging.getLogger('cps.web').debug('Create-Dir: '+dst_dir)
|
||||
if change_permissions:
|
||||
# print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid))
|
||||
os.chown(dst_dir, new_permissions.st_uid, new_permissions.st_gid)
|
||||
for file_ in files:
|
||||
src_file = os.path.join(src_dir, file_)
|
||||
dst_file = os.path.join(dst_dir, file_)
|
||||
if os.path.exists(dst_file):
|
||||
if change_permissions:
|
||||
permission = os.stat(dst_file)
|
||||
logging.getLogger('cps.web').debug('Remove file before copy: '+dst_file)
|
||||
os.remove(dst_file)
|
||||
else:
|
||||
if change_permissions:
|
||||
permission = new_permissions
|
||||
shutil.move(src_file, dst_dir)
|
||||
logging.getLogger('cps.web').debug('Move File '+src_file+' to '+dst_dir)
|
||||
if change_permissions:
|
||||
try:
|
||||
os.chown(dst_file, permission.st_uid, permission.st_gid)
|
||||
except (Exception) as e:
|
||||
# ex = sys.exc_info()
|
||||
old_permissions = os.stat(dst_file)
|
||||
logging.getLogger('cps.web').debug('Fail change permissions of ' + str(dst_file) + '. Before: '
|
||||
+ str(old_permissions.st_uid) + ':' + str(old_permissions.st_gid) + ' After: '
|
||||
+ str(permission.st_uid) + ':' + str(permission.st_gid) + ' error: '+str(e))
|
||||
return
|
||||
|
||||
def update_source(self, source, destination):
|
||||
# destination files
|
||||
old_list = list()
|
||||
exclude = (
|
||||
'vendor' + os.sep + 'kindlegen.exe', 'vendor' + os.sep + 'kindlegen', os.sep + 'app.db',
|
||||
os.sep + 'vendor', os.sep + 'calibre-web.log')
|
||||
for root, dirs, files in os.walk(destination, topdown=True):
|
||||
for name in files:
|
||||
old_list.append(os.path.join(root, name).replace(destination, ''))
|
||||
for name in dirs:
|
||||
old_list.append(os.path.join(root, name).replace(destination, ''))
|
||||
# source files
|
||||
new_list = list()
|
||||
for root, dirs, files in os.walk(source, topdown=True):
|
||||
for name in files:
|
||||
new_list.append(os.path.join(root, name).replace(source, ''))
|
||||
for name in dirs:
|
||||
new_list.append(os.path.join(root, name).replace(source, ''))
|
||||
|
||||
delete_files = self.one_minus_two(old_list, new_list)
|
||||
|
||||
rf = self.reduce_files(delete_files, exclude)
|
||||
|
||||
remove_items = self.reduce_dirs(rf, new_list)
|
||||
|
||||
self.moveallfiles(source, destination)
|
||||
|
||||
for item in remove_items:
|
||||
item_path = os.path.join(destination, item[1:])
|
||||
if os.path.isdir(item_path):
|
||||
logging.getLogger('cps.web').debug("Delete dir " + item_path)
|
||||
shutil.rmtree(item_path)
|
||||
else:
|
||||
try:
|
||||
logging.getLogger('cps.web').debug("Delete file " + item_path)
|
||||
# log_from_thread("Delete file " + item_path)
|
||||
os.remove(item_path)
|
||||
except Exception:
|
||||
logging.getLogger('cps.web').debug("Could not remove:" + item_path)
|
||||
shutil.rmtree(source, ignore_errors=True)
|
||||
|
||||
|
||||
def check_unrar(unrarLocation):
|
||||
error = False
|
||||
if os.path.exists(unrarLocation):
|
||||
@ -548,22 +512,55 @@ def check_unrar(unrarLocation):
|
||||
return (error, version)
|
||||
|
||||
|
||||
def is_sha1(sha1):
|
||||
if len(sha1) != 40:
|
||||
return False
|
||||
try:
|
||||
int(sha1, 16)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def json_serial(obj):
|
||||
"""JSON serializer for objects not serializable by default json code"""
|
||||
|
||||
if isinstance(obj, (datetime)):
|
||||
return obj.isoformat()
|
||||
raise TypeError ("Type %s not serializable" % type(obj))
|
||||
|
||||
|
||||
def get_current_version_info():
|
||||
content = {}
|
||||
content[0] = '$Format:%H$'
|
||||
content[1] = '$Format:%cI$'
|
||||
# content[0] = 'bb7d2c6273ae4560e83950d36d64533343623a57'
|
||||
# content[1] = '2018-09-09T10:13:08+02:00'
|
||||
if is_sha1(content[0]) and len(content[1]) > 0:
|
||||
return {'hash': content[0], 'datetime': content[1]}
|
||||
return False
|
||||
def render_task_status(tasklist):
|
||||
#helper function to apply localize status information in tasklist entries
|
||||
renderedtasklist=list()
|
||||
# task2 = task
|
||||
for task in tasklist:
|
||||
if task['user'] == current_user.nickname or current_user.role_admin():
|
||||
# task2 = copy.deepcopy(task) # = task
|
||||
if task['formStarttime']:
|
||||
task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=web.get_locale())
|
||||
# task2['formStarttime'] = ""
|
||||
else:
|
||||
if 'starttime' not in task:
|
||||
task['starttime'] = ""
|
||||
|
||||
# localize the task status
|
||||
if isinstance( task['stat'], int ):
|
||||
if task['stat'] == worker.STAT_WAITING:
|
||||
task['status'] = _(u'Waiting')
|
||||
elif task['stat'] == worker.STAT_FAIL:
|
||||
task['status'] = _(u'Failed')
|
||||
elif task['stat'] == worker.STAT_STARTED:
|
||||
task['status'] = _(u'Started')
|
||||
elif task['stat'] == worker.STAT_FINISH_SUCCESS:
|
||||
task['status'] = _(u'Finished')
|
||||
else:
|
||||
task['status'] = _(u'Unknown Status')
|
||||
|
||||
# localize the task type
|
||||
if isinstance( task['taskType'], int ):
|
||||
if task['taskType'] == worker.TASK_EMAIL:
|
||||
task['taskMessage'] = _(u'E-mail: ') + task['taskMess']
|
||||
elif task['taskType'] == worker.TASK_CONVERT:
|
||||
task['taskMessage'] = _(u'Convert: ') + task['taskMess']
|
||||
elif task['taskType'] == worker.TASK_UPLOAD:
|
||||
task['taskMessage'] = _(u'Upload: ') + task['taskMess']
|
||||
elif task['taskType'] == worker.TASK_CONVERT_ANY:
|
||||
task['taskMessage'] = _(u'Convert: ') + task['taskMess']
|
||||
else:
|
||||
task['taskMessage'] = _(u'Unknown Task: ') + task['taskMess']
|
||||
|
||||
renderedtasklist.append(task)
|
||||
|
||||
return renderedtasklist
|
||||
|
@ -1,3 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Flask License
|
||||
#
|
||||
# Copyright © 2010 by the Pallets team.
|
||||
#
|
||||
# Some rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms of the software as well as
|
||||
# documentation, with or without modification, are permitted provided that the
|
||||
# following conditions are met:
|
||||
#
|
||||
# Redistributions of source code must retain the above copyright notice, this list of conditions
|
||||
# and the following disclaimer.
|
||||
# Redistributions in binary form must reproduce the above copyright notice, this list of conditions
|
||||
# and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
# Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR
|
||||
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# http://flask.pocoo.org/snippets/62/
|
||||
|
||||
try:
|
||||
|
@ -1,6 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2018 cervinko, janeczku, 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/>.
|
||||
|
||||
class ReverseProxied(object):
|
||||
"""Wrap the application in this middleware and configure the
|
||||
front-end server to add these headers, to let you quietly bind
|
||||
|
107
cps/server.py
107
cps/server.py
@ -1,6 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2012-2019 janeczku, OzzieIsaacs, andrerfcsantos, idalin
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
|
||||
from socket import error as SocketError
|
||||
import sys
|
||||
import os
|
||||
@ -20,6 +37,7 @@ except ImportError:
|
||||
gevent_present = False
|
||||
|
||||
|
||||
|
||||
class server:
|
||||
|
||||
wsgiserver = None
|
||||
@ -32,19 +50,32 @@ class server:
|
||||
def start_gevent(self):
|
||||
try:
|
||||
ssl_args = dict()
|
||||
if web.ub.config.get_config_certfile() and web.ub.config.get_config_keyfile():
|
||||
ssl_args = {"certfile": web.ub.config.get_config_certfile(),
|
||||
"keyfile": web.ub.config.get_config_keyfile()}
|
||||
certfile_path = web.ub.config.get_config_certfile()
|
||||
keyfile_path = web.ub.config.get_config_keyfile()
|
||||
if certfile_path and keyfile_path:
|
||||
if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path):
|
||||
ssl_args = {"certfile": certfile_path,
|
||||
"keyfile": keyfile_path}
|
||||
else:
|
||||
web.app.logger.info('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl. Cert path: %s | Key path: %s' % (certfile_path, keyfile_path))
|
||||
if os.name == 'nt':
|
||||
self.wsgiserver= WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
|
||||
else:
|
||||
self.wsgiserver = WSGIServer(('', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
|
||||
web.py3_gevent_link = self.wsgiserver
|
||||
self.wsgiserver.serve_forever()
|
||||
|
||||
except SocketError:
|
||||
web.app.logger.info('Unable to listen on \'\', trying on IPv4 only...')
|
||||
self.wsgiserver = WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
|
||||
self.wsgiserver.serve_forever()
|
||||
try:
|
||||
web.app.logger.info('Unable to listen on \'\', trying on IPv4 only...')
|
||||
self.wsgiserver = WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
|
||||
web.py3_gevent_link = self.wsgiserver
|
||||
self.wsgiserver.serve_forever()
|
||||
except (OSError, SocketError) as e:
|
||||
web.app.logger.info("Error starting server: %s" % e.strerror)
|
||||
print("Error starting server: %s" % e.strerror)
|
||||
web.helper.global_WorkerThread.stop()
|
||||
sys.exit(1)
|
||||
except Exception:
|
||||
web.app.logger.info("Unknown error while starting gevent")
|
||||
|
||||
@ -54,22 +85,36 @@ class server:
|
||||
# leave subprocess out to allow forking for fetchers and processors
|
||||
self.start_gevent()
|
||||
else:
|
||||
web.app.logger.info('Starting Tornado server')
|
||||
if web.ub.config.get_config_certfile() and web.ub.config.get_config_keyfile():
|
||||
ssl={"certfile": web.ub.config.get_config_certfile(),
|
||||
"keyfile": web.ub.config.get_config_keyfile()}
|
||||
else:
|
||||
ssl=None
|
||||
# Max Buffersize set to 200MB
|
||||
http_server = HTTPServer(WSGIContainer(web.app),
|
||||
max_buffer_size = 209700000,
|
||||
ssl_options=ssl)
|
||||
http_server.listen(web.ub.config.config_port)
|
||||
self.wsgiserver=IOLoop.instance()
|
||||
self.wsgiserver.start()
|
||||
# wait for stop signal
|
||||
self.wsgiserver.close(True)
|
||||
try:
|
||||
ssl = None
|
||||
web.app.logger.info('Starting Tornado server')
|
||||
certfile_path = web.ub.config.get_config_certfile()
|
||||
keyfile_path = web.ub.config.get_config_keyfile()
|
||||
if certfile_path and keyfile_path:
|
||||
if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path):
|
||||
ssl = {"certfile": certfile_path,
|
||||
"keyfile": keyfile_path}
|
||||
else:
|
||||
web.app.logger.info('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl. Cert path: %s | Key path: %s' % (certfile_path, keyfile_path))
|
||||
|
||||
# Max Buffersize set to 200MB
|
||||
http_server = HTTPServer(WSGIContainer(web.app),
|
||||
max_buffer_size = 209700000,
|
||||
ssl_options=ssl)
|
||||
http_server.listen(web.ub.config.config_port)
|
||||
self.wsgiserver=IOLoop.instance()
|
||||
self.wsgiserver.start()
|
||||
# wait for stop signal
|
||||
self.wsgiserver.close(True)
|
||||
except SocketError as e:
|
||||
web.app.logger.info("Error starting server: %s" % e.strerror)
|
||||
print("Error starting server: %s" % e.strerror)
|
||||
web.helper.global_WorkerThread.stop()
|
||||
sys.exit(1)
|
||||
|
||||
# ToDo: Somehow caused by circular import under python3 refactor
|
||||
if sys.version_info > (3, 0):
|
||||
self.restart = web.py3_restart_Typ
|
||||
if self.restart == True:
|
||||
web.app.logger.info("Performing restart of Calibre-Web")
|
||||
web.helper.global_WorkerThread.stop()
|
||||
@ -86,16 +131,26 @@ class server:
|
||||
sys.exit(0)
|
||||
|
||||
def setRestartTyp(self,starttyp):
|
||||
self.restart=starttyp
|
||||
self.restart = starttyp
|
||||
# ToDo: Somehow caused by circular import under python3 refactor
|
||||
web.py3_restart_Typ = starttyp
|
||||
|
||||
def killServer(self, signum, frame):
|
||||
self.stopServer()
|
||||
|
||||
def stopServer(self):
|
||||
if gevent_present:
|
||||
self.wsgiserver.close()
|
||||
else:
|
||||
self.wsgiserver.add_callback(self.wsgiserver.stop)
|
||||
# ToDo: Somehow caused by circular import under python3 refactor
|
||||
if sys.version_info > (3, 0):
|
||||
if not self.wsgiserver:
|
||||
if gevent_present:
|
||||
self.wsgiserver = web.py3_gevent_link
|
||||
else:
|
||||
self.wsgiserver = IOLoop.instance()
|
||||
if self.wsgiserver:
|
||||
if gevent_present:
|
||||
self.wsgiserver.close()
|
||||
else:
|
||||
self.wsgiserver.add_callback(self.wsgiserver.stop)
|
||||
|
||||
@staticmethod
|
||||
def getNameVersion():
|
||||
|
File diff suppressed because it is too large
Load Diff
1
cps/static/css/caliBlur.min.css
vendored
Normal file
1
cps/static/css/caliBlur.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
cps/static/css/images/caliblur/blur-dark.png
Normal file
BIN
cps/static/css/images/caliblur/blur-dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
BIN
cps/static/css/images/caliblur/blur-light.png
Normal file
BIN
cps/static/css/images/caliblur/blur-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
BIN
cps/static/css/images/caliblur/blur-noise.png
Normal file
BIN
cps/static/css/images/caliblur/blur-noise.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 92 KiB |
File diff suppressed because one or more lines are too long
6
cps/static/css/libs/bootstrap-theme.min.css
vendored
6
cps/static/css/libs/bootstrap-theme.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6
cps/static/css/libs/bootstrap.min.css
vendored
6
cps/static/css/libs/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
2
cps/static/css/libs/bootstrap.min.css.map
vendored
2
cps/static/css/libs/bootstrap.min.css.map
vendored
File diff suppressed because one or more lines are too long
638
cps/static/css/libs/normalize.css
vendored
638
cps/static/css/libs/normalize.css
vendored
@ -1,505 +1,349 @@
|
||||
/*! normalize.css v1.0.1 | MIT License | git.io/normalize */
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* ==========================================================================
|
||||
HTML5 display definitions
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Corrects `block` display not defined in IE 6/7/8/9 and Firefox 3.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Corrects `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
video {
|
||||
display: inline-block;
|
||||
*display: inline;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Prevents modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses styling for `hidden` attribute not present in IE 7/8/9, Firefox 3,
|
||||
* and Safari 4.
|
||||
* Known issue: no IE 6 support.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Base
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* 1. Corrects text resizing oddly in IE 6/7 when body `font-size` is set using
|
||||
* `em` units.
|
||||
* 2. Prevents iOS text size adjust after orientation change, without disabling
|
||||
* user zoom.
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-size: 100%; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses `font-family` inconsistency between `textarea` and other form
|
||||
* elements.
|
||||
*/
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
html,
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses margins handled incorrectly in IE 6/7.
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Links
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Addresses `outline` inconsistency between Chrome and other browsers.
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
a:focus {
|
||||
outline: thin dotted;
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Improves readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Typography
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Addresses font sizes and margins set differently in IE 6/7.
|
||||
* Addresses font sizes within `section` and `article` in Firefox 4+, Safari 5,
|
||||
* and Chrome.
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
margin: 0.83em 0;
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.17em;
|
||||
margin: 1em 0;
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
margin: 1.33em 0;
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 0.83em;
|
||||
margin: 1.67em 0;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.75em;
|
||||
margin: 2.33em 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses styling not present in IE 7/8/9, Safari 5, and Chrome.
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome.
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses styling not present in Safari 5 and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses styling not present in IE 6/7/8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses margins set differently in IE 6/7.
|
||||
*/
|
||||
|
||||
p,
|
||||
pre {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Corrects font family set oddly in IE 6, Safari 4/5, and Chrome.
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, serif;
|
||||
_font-family: 'courier new', monospace;
|
||||
font-size: 1em;
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
* Improves readability of pre-formatted text in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses CSS quotes not supported in IE 6/7.
|
||||
*/
|
||||
|
||||
q {
|
||||
quotes: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses `quotes` property not supported in Safari 4.
|
||||
*/
|
||||
|
||||
q:before,
|
||||
q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses inconsistent and variable font size in all browsers.
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
* Prevents `sub` and `sup` affecting `line-height` in all browsers.
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Lists
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Addresses margins set differently in IE 6/7.
|
||||
*/
|
||||
|
||||
dl,
|
||||
menu,
|
||||
ol,
|
||||
ul {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0 0 0 40px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses paddings set differently in IE 6/7.
|
||||
*/
|
||||
|
||||
menu,
|
||||
ol,
|
||||
ul {
|
||||
padding: 0 0 0 40px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Corrects list images handled incorrectly in IE 7.
|
||||
*/
|
||||
|
||||
nav ul,
|
||||
nav ol {
|
||||
list-style: none;
|
||||
list-style-image: none;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* 1. Removes border when inside `a` element in IE 6/7/8/9 and Firefox 3.
|
||||
* 2. Improves image quality when scaled in IE 7.
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0; /* 1 */
|
||||
-ms-interpolation-mode: bicubic; /* 2 */
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Corrects overflow displayed oddly in IE 9.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Figures
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Addresses margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Forms
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Corrects margin displayed oddly in IE 6/7.
|
||||
*/
|
||||
|
||||
form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Corrects color not being inherited in IE 6/7/8/9.
|
||||
* 2. Corrects text not wrapping in Firefox 3.
|
||||
* 3. Corrects alignment displayed oddly in IE 6/7.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0;
|
||||
white-space: normal; /* 2 */
|
||||
*margin-left: -7px; /* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Corrects font size not being inherited in all browsers.
|
||||
* 2. Addresses margins set differently in IE 6/7, Firefox 3+, Safari 5,
|
||||
* and Chrome.
|
||||
* 3. Improves appearance and consistency in all browsers.
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-size: 100%; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
vertical-align: baseline; /* 3 */
|
||||
*vertical-align: middle; /* 3 */
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses Firefox 3+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input {
|
||||
line-height: normal;
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Corrects inability to style clickable `input` types in iOS.
|
||||
* 3. Improves usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
* 4. Removes inner spacing in IE 7 without affecting normal text inputs.
|
||||
* Known issue: inner spacing remains in IE 6.
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
*overflow: visible; /* 4 */
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Re-set default cursor for disabled elements.
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
input[disabled] {
|
||||
cursor: default;
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Addresses box sizing set to content-box in IE 8/9.
|
||||
* 2. Removes excess padding in IE 8/9.
|
||||
* 3. Removes excess padding in IE 7.
|
||||
* Known issue: excess padding remains in IE 6.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
*height: 13px; /* 3 */
|
||||
*width: 13px; /* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome.
|
||||
* 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome
|
||||
* (include `-moz` to future-proof).
|
||||
*/
|
||||
/*
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield;
|
||||
-moz-box-sizing: content-box;
|
||||
-webkit-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
* Removes inner padding and search cancel button in Safari 5 and Chrome
|
||||
* on OS X.
|
||||
*/
|
||||
|
||||
/* input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
} */
|
||||
|
||||
/*
|
||||
* Removes inner padding and border in Firefox 3+.
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Removes default vertical scrollbar in IE 6/7/8/9.
|
||||
* 2. Improves readability and alignment in all browsers.
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto; /* 1 */
|
||||
vertical-align: top; /* 2 */
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Tables
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Remove most spacing between table cells.
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
@ -5,6 +5,22 @@
|
||||
src: local('Grand Hotel'), local('GrandHotel-Regular'), url("fonts/GrandHotel-Regular.ttf") format('truetype');
|
||||
}
|
||||
|
||||
html.http-error {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
.http-error body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
.http-error body > div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body{background:#f2f2f2}body h2{font-weight:normal;color:#444}
|
||||
body { margin-bottom: 40px;}
|
||||
a{color: #45b29d}a:hover{color: #444;}
|
||||
@ -20,6 +36,11 @@ a{color: #45b29d}a:hover{color: #444;}
|
||||
.container-fluid .book .meta .title{font-weight:bold;font-size:15px;color:#444}
|
||||
.container-fluid .book .meta .author{font-size:12px;color:#999}
|
||||
.container-fluid .book .meta .rating{margin-top:5px}.rating .glyphicon-star{color:#999}.rating .glyphicon-star.good{color:#45b29d}
|
||||
|
||||
.container-fluid .author .author-hidden, .container-fluid .author .author-hidden-divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-brand{font-family: 'Grand Hotel', cursive; font-size: 35px; color: #45b29d !important;}
|
||||
.more-stuff{margin-top: 20px; padding-top: 20px; border-top: 1px solid #ccc}
|
||||
.more-stuff>li{margin-bottom: 10px;}
|
||||
@ -36,6 +57,7 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te
|
||||
-moz-box-shadow: 0 5px 8px -6px #777;
|
||||
box-shadow: 0 5px 8px -6px #777;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-toggle .icon-bar {background-color: #000;}
|
||||
.navbar-default .navbar-toggle {border-color: #000;}
|
||||
.cover { margin-bottom: 10px;}
|
||||
@ -87,6 +109,16 @@ input.pill:not(:checked) + label .glyphicon {
|
||||
|
||||
.tags_click, .serie_click, .language_click {margin-right: 5px;}
|
||||
|
||||
#meta-info {
|
||||
height:600px;
|
||||
overflow-y:scroll;
|
||||
}
|
||||
.media-list {
|
||||
padding-right:15px;
|
||||
}
|
||||
.media-body p {
|
||||
text-align: justify;
|
||||
}
|
||||
#meta-info img { max-height: 150px; max-width: 100px; cursor: pointer; }
|
||||
|
||||
.padded-bottom { margin-bottom: 15px; }
|
||||
@ -104,3 +136,7 @@ input.pill:not(:checked) + label .glyphicon {
|
||||
.editable-cancel { margin-bottom: 0px !important; margin-left: 7px !important;}
|
||||
.editable-submit { margin-bottom: 0px !important;}
|
||||
|
||||
.modal-body .comments {
|
||||
max-height:300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
8
cps/static/css/upload.css
Normal file
8
cps/static/css/upload.css
Normal file
@ -0,0 +1,8 @@
|
||||
@media (min-device-width: 768px) {
|
||||
.upload-modal-dialog {
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
}
|
||||
}
|
@ -277,8 +277,6 @@ bitjs.archive = bitjs.archive || {};
|
||||
if (e.type === bitjs.archive.UnarchiveEvent.Type.FINISH) {
|
||||
this.worker_.terminate();
|
||||
}
|
||||
} else {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
@ -292,15 +290,11 @@ bitjs.archive = bitjs.archive || {};
|
||||
this.worker_ = new Worker(scriptFileName);
|
||||
|
||||
this.worker_.onerror = function(e) {
|
||||
console.log("Worker error: message = " + e.message);
|
||||
throw e;
|
||||
};
|
||||
|
||||
this.worker_.onmessage = function(e) {
|
||||
if (typeof e.data === "string") {
|
||||
// Just log any strings the workers pump our way.
|
||||
console.log(e.data);
|
||||
} else {
|
||||
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);
|
||||
|
708
cps/static/js/caliBlur.js
Normal file
708
cps/static/js/caliBlur.js
Normal file
@ -0,0 +1,708 @@
|
||||
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
* Copyright (C) 2018-2019 hexeth
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
// Move advanced search to side-menu
|
||||
$( 'a[href*="advanced"]' ).parent().insertAfter( '#nav_new' );
|
||||
$( 'body' ).addClass('blur');
|
||||
$( 'body.stat' ).addClass( 'stats' );
|
||||
$( 'body.config' ).addClass( 'admin');
|
||||
$( 'body.uiconfig' ).addClass( 'admin');
|
||||
$( 'body.advsearch' ).addClass( 'advanced_search' );
|
||||
$( 'body.newuser' ).addClass( 'admin' );
|
||||
$( 'body.mailset' ).addClass( 'admin' );
|
||||
|
||||
// Back button
|
||||
curHref = window.location.href.split('/');
|
||||
prevHref = document.referrer.split('/');
|
||||
$( '.navbar-form.navbar-left' )
|
||||
.before( '<div class="plexBack"><a href="' + document.referrer + '"></a></div>' );
|
||||
if ( history.length === 1 ||
|
||||
curHref[0] +
|
||||
curHref[1] +
|
||||
curHref[2] !=
|
||||
prevHref[0] +
|
||||
prevHref[1] +
|
||||
prevHref[2] ||
|
||||
$( 'body.root' )>length > 0 ) {
|
||||
$( '.plexBack' ).addClass( 'noBack' );
|
||||
}
|
||||
|
||||
//Weird missing a after pressing back from edit.
|
||||
setTimeout(function() {
|
||||
if ( $( '.plexBack a').length < 1 ) {
|
||||
$( '.plexBack' ).append('<a href="' + document.referrer + '"></a>');
|
||||
}
|
||||
},10);
|
||||
|
||||
// Home button
|
||||
$( '.plexBack' ).before( '<div class="home-btn"></div>' );
|
||||
$( 'a.navbar-brand' ).clone().appendTo( '.home-btn' ).empty().removeClass('navbar-brand');
|
||||
/////////////////////////////////
|
||||
// Start of Book Details Work //
|
||||
///////////////////////////////
|
||||
|
||||
// Wrap book description in div container
|
||||
if ( $( 'body.book' ).length > 0 ) {
|
||||
|
||||
description = $( '.comments' );
|
||||
bookInfo = $( '.author' ).nextUntil( 'h3:contains("Description")');
|
||||
$( 'h3:contains("Description")' ).detach();
|
||||
$( '.comments' ).detach();
|
||||
$( bookInfo ).wrapAll( '<div class="bookinfo"></div>' );
|
||||
// $( 'h3:contains("Description:")' ).after( '<div class="description"></div>' );
|
||||
$( '.languages' ).appendTo( '.bookinfo' );
|
||||
$('.hr').detach();
|
||||
if ( $( '.identifiers ').length > 0 ) {
|
||||
console.log(".identifiers length " + $( '.identifiers ').length );
|
||||
$( '.identifiers' ).before( '<div class="hr"></div>' );
|
||||
} else {
|
||||
if ( $( '.bookinfo > p:first-child' ).length > 0 ) {
|
||||
console.log(".bookinfo > p:first-child length " + $( '.bookinfo > p' ).length );
|
||||
$( '.bookinfo > p:first-child' ).first().after( '<div class="hr"></div>' );
|
||||
} else{
|
||||
if ( $( '.bookinfo a[href*="/series/"]' ).length > 0 ) {
|
||||
console.log( 'series text found; placing hr below series' );
|
||||
$( '.bookinfo a[href*="/series/"]' ).parent().after( '<div class="hr"></div>' );
|
||||
} else {
|
||||
console.log("prepending hr div to top of .bookinfo");
|
||||
$( '.bookinfo' ).prepend( '<div class="hr"></div>' );
|
||||
}
|
||||
}
|
||||
}
|
||||
$( '.rating' ).insertBefore( '.hr' );
|
||||
$( '#remove-from-shelves' ).insertAfter( '.hr' );
|
||||
$( description ).appendTo('.bookinfo')
|
||||
/* if book description is not in html format, Remove extra line breaks
|
||||
Remove blank lines/unnecessary spaces, split by line break to array
|
||||
Push array into .description div. If there is still a wall of text,
|
||||
find sentences and split wall into groups of three sentence paragraphs.
|
||||
If the book format is in html format, Keep html, but strip away inline
|
||||
styles and empty elements */
|
||||
|
||||
// If text is sitting in div as text node
|
||||
if ( $('.comments:has(p)' ).length === 0 ) {
|
||||
newdesc = description.text()
|
||||
.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm,"").split(/\n/);
|
||||
$('.comments' ).empty();
|
||||
$.each(newdesc, function(i, val) {
|
||||
$( 'div.comments' ).append( '<p>' + newdesc[i] + '</p>' );
|
||||
});
|
||||
$( '.comments' ).fadeIn(100);
|
||||
} //If still a wall of text create 3 sentence paragraphs.
|
||||
if( $( '.comments p' ).length === 1 ) {
|
||||
if ( description.context != undefined ) {
|
||||
newdesc = description.text()
|
||||
.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm,"").split(/\n/);
|
||||
}
|
||||
else {
|
||||
newdesc = description.text();
|
||||
}
|
||||
doc = nlp ( newdesc.toString() );
|
||||
sentences = doc.map((m)=> m.out( 'text' ));
|
||||
sentences[0] = sentences[0].replace(",","");
|
||||
$( '.comments p' ).remove();
|
||||
let size = 3; let sentenceChunks = [];
|
||||
for (var i=0; i<sentences.length; i+=size) {
|
||||
sentenceChunks.push(sentences.slice(i,i+size));
|
||||
}
|
||||
let output = '';
|
||||
$.each(sentenceChunks, function(i, val) {
|
||||
let preOutput = '';
|
||||
$.each(val, function(i, val) {
|
||||
preOutput += val;
|
||||
});
|
||||
output += '<p>' + preOutput + '</p>';
|
||||
});
|
||||
$( 'div.comments' ).append( output );
|
||||
}
|
||||
else {
|
||||
$.each(description, function(i, val) {
|
||||
// $( description[i].outerHTML ).appendTo( '.comments' );
|
||||
$( 'div.comments :empty' ).remove();
|
||||
$( 'div.comments ').attr( 'style', '' );
|
||||
});
|
||||
$( 'div.comments' ).fadeIn( 100 );
|
||||
}
|
||||
|
||||
// Sexy blurred backgrounds
|
||||
cover = $( '.cover img' ).attr( 'src' );
|
||||
$( '#loader + .container-fluid' )
|
||||
.prepend( '<div class="blur-wrapper"></div' );
|
||||
$( '.blur-wrapper' )
|
||||
.prepend( '<div><img class="bg-blur" src="' + cover + '"></div>' );
|
||||
|
||||
// Fix-up book detail headings
|
||||
publisher = $( '.publishers p span' ).text().split( ':' );
|
||||
$( '.publishers p span' ).remove();
|
||||
$.each(publisher, function(i, val) {
|
||||
$( '.publishers' ).append( '<span>' + publisher[i] + '</span>' );
|
||||
});
|
||||
$( '.publishers span:nth-child(3)' ).text(function() {
|
||||
return $(this).text().replace(/^\s+|^\t+|\t+|\s+$/g, "");
|
||||
});
|
||||
|
||||
published = $( '.publishing-date p' )
|
||||
.text().split(': ');
|
||||
$( '.publishing-date p' ).remove();
|
||||
$.each(published, function(i, val) {
|
||||
$( '.publishing-date' ).append( '<span>' + published[i] + '</span>' );
|
||||
});
|
||||
|
||||
languages = $( '.languages p span' ).text().split( ': ' );
|
||||
$( '.languages p span' ).remove();
|
||||
$.each(languages, function(i, val) {
|
||||
$( '.languages' ).append( '<span>' + languages[i] + '</span>' );
|
||||
});
|
||||
|
||||
$( '.book-meta h2:first' ).clone()
|
||||
.prependTo( '.book-meta > .btn-toolbar:first' );
|
||||
|
||||
// If only one download type exists still put the items into a drop-drown list.
|
||||
downloads = $( 'a[id^=btnGroupDrop]' ).get();
|
||||
if ( $( downloads ).length === 1 ) {
|
||||
$( '<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-download"></span>Download :<span class="caret"></span></button><ul class="dropdown-menu leramslist aria-labelledby="btnGroupDrop1"></ul>' ).insertBefore( downloads[downloads.length-1] );
|
||||
$( downloads ).detach();
|
||||
$.each(downloads, function(i, val) {
|
||||
$( '<li>' + downloads[i].outerHTML + '</li>' ).appendTo( '.leramslist' );
|
||||
});
|
||||
$( '.leramslist' ).find( 'span' ).remove();
|
||||
$( '.leramslist a' ).removeClass( 'btn btn-primary' ).removeAttr( 'role' );
|
||||
}
|
||||
|
||||
// Add classes to buttons
|
||||
$( '#sendbtn' ).parent().addClass( 'sendBtn' );
|
||||
$( '[id*=btnGroupDrop]' ).parent().addClass( 'downloadBtn' );
|
||||
$( 'read-in-browser' ).parent().addClass( 'readBtn' );
|
||||
$( '.downloadBtn button:first' ).addClass( 'download-text' );
|
||||
|
||||
// Move all options in book details page to the same group
|
||||
$( '[aria-label*="Delete book"]' )
|
||||
.prependTo( '[aria-label^="Download, send"]' )
|
||||
.children().removeClass( 'btn-sm' );
|
||||
$( '.custom_columns' )
|
||||
.addClass(' btn-group' )
|
||||
.attr('role', 'group' )
|
||||
.removeClass( 'custom_columns' )
|
||||
.prependTo( '[aria-label^="Download, send"]' );
|
||||
$( '#have_read_cb' )
|
||||
.after( '<label class="block-label readLbl" for="#have_read_cb"></label>' );
|
||||
$( '#shelf-actions' ).prependTo( '[aria-label^="Download, send"]' );
|
||||
|
||||
|
||||
// Move dropdown lists higher in dom, replace bootstrap toggle with own toggle.
|
||||
$( 'ul[aria-labelledby="read-in-browser"]' ).insertBefore( '.blur-wrapper' ).addClass('readinbrowser-drop');
|
||||
$( 'ul[aria-labelledby="send-to-kindle"]' ).insertBefore( '.blur-wrapper' ).addClass('sendtokindle-drop');
|
||||
$( '.leramslist' ).insertBefore( '.blur-wrapper' );
|
||||
$( 'ul[aria-labelledby="btnGroupDrop1"]' ).insertBefore( '.blur-wrapper' ).addClass('leramslist');
|
||||
$( '#add-to-shelves' ).insertBefore( '.blur-wrapper' );
|
||||
|
||||
$( '#read-in-browser' ).click( function() {
|
||||
$( '.readinbrowser-drop' ).toggle();
|
||||
});
|
||||
|
||||
$('.downloadBtn' ).click( function() {
|
||||
$( '.leramslist' ).toggle();
|
||||
});
|
||||
|
||||
$('#sendbtn2' ).click( function() {
|
||||
$( '.sendtokindle-drop' ).toggle();
|
||||
});
|
||||
|
||||
|
||||
$('div[aria-label="Add to shelves"]' ).click( function() {
|
||||
$( '#add-to-shelves' ).toggle();
|
||||
});
|
||||
|
||||
// Fix formatting error on book detail languages
|
||||
if ( !$( '.book-meta > .bookinfo > .languages > span:last-of-type' ).text().startsWith(" ") ) {
|
||||
$( '.book-meta > .bookinfo > .languages > span:last-of-type' ).prepend(" ");
|
||||
}
|
||||
|
||||
//Work to reposition dropdowns. Does not currently solve for
|
||||
//screen resizing
|
||||
function dropdownToggle() {
|
||||
|
||||
topPos = $( '.book-meta > .btn-toolbar:first' ).offset().top
|
||||
|
||||
if ( $( '#read-in-browser' ).length > 0 ) {
|
||||
position = $( '#read-in-browser' ).offset().left
|
||||
if ( position + $( '.readinbrowser-drop' ).width() > $( window ).width() ) {
|
||||
positionOff = position + $( '.readinbrowser-drop' ).width() - $( window ).width();
|
||||
ribPosition = position - positionOff - 5
|
||||
$( '.readinbrowser-drop' ).attr("style", "left: " + ribPosition + "px !important; right: auto; top: " + topPos + "px");
|
||||
} else {
|
||||
$( '.readinbrowser-drop' ).attr("style", "left: " + position + "px !important; right: auto; top: " + topPos + "px");
|
||||
}
|
||||
}
|
||||
|
||||
if ( $( '#sendbtn2' ).length > 0 ) {
|
||||
position = $( '#sendbtn2' ).offset().left
|
||||
if ( position + $( '.sendtokindle-drop' ).width() > $( window ).width() ) {
|
||||
positionOff = position + $( '.sendtokindle-drop' ).width() - $( window ).width();
|
||||
ribPosition = position - positionOff - 5
|
||||
$( '.sendtokindle-drop' ).attr("style", "left: " + ribPosition + "px !important; right: auto; top: " + topPos + "px");
|
||||
} else {
|
||||
$( '.sendtokindle-drop' ).attr("style", "left: " + position + "px !important; right: auto; top: " + topPos + "px");
|
||||
}
|
||||
}
|
||||
|
||||
if ( $( '.downloadBtn' ).length > 0 ) {
|
||||
|
||||
position = $( '#btnGroupDrop1' ).offset().left
|
||||
|
||||
if ( position + $( '.leramslist' ).width() > $( window ).width() ) {
|
||||
positionOff = position + $( '.leramslist' ).width() - $( window ).width();
|
||||
dlPosition = position - positionOff - 5
|
||||
$( '.leramslist' ).attr("style", "left: " + dlPosition + "px !important; right: auto; top: " + topPos + "px");
|
||||
} else {
|
||||
$( '.leramslist' ).attr("style", "left: " + position + "px !important; right: auto; top: " + topPos + "px");
|
||||
}
|
||||
}
|
||||
|
||||
if ( $( 'div[aria-label="Add to shelves"]' ).length > 0 ) {
|
||||
|
||||
position = $( 'div[aria-label="Add to shelves"]' ).offset().left
|
||||
|
||||
if ( position + $( '#add-to-shelves' ).width() > $( window ).width() ) {
|
||||
positionOff = position + $( '#add-to-shelves' ).width() - $( window ).width();
|
||||
adsPosition = position - positionOff - 5
|
||||
$( '#add-to-shelves' ).attr("style", "left: " + adsPosition + "px !important; right: auto; top: " + topPos + "px");
|
||||
} else {
|
||||
$( '#add-to-shelves' ).attr("style", "left: " + position + "px !important; right: auto; top: " + topPos + "px");
|
||||
}
|
||||
}
|
||||
}
|
||||
dropdownToggle();
|
||||
|
||||
$( window ).on( 'resize', function() {
|
||||
dropdownToggle();
|
||||
});
|
||||
|
||||
// Clone book rating for mobile view.
|
||||
$( '.book-meta > .bookinfo > .rating' ).clone().insertBefore( '.book-meta > .description' ).addClass('rating-mobile');
|
||||
}
|
||||
|
||||
///////////////////////////////
|
||||
// End of Book Details Work //
|
||||
/////////////////////////////
|
||||
|
||||
/////////////////////////////////
|
||||
// Start of Global Work //
|
||||
///////////////////////////////
|
||||
|
||||
// Hide dropdown and collapse menus on click-off
|
||||
$(document).mouseup(function (e) {
|
||||
var container = new Array();
|
||||
container.push($('ul[aria-labelledby="read-in-browser"]'));
|
||||
container.push($('.sendtokindle-drop'));
|
||||
container.push($('.leramslist'));
|
||||
container.push($('#add-to-shelves'));
|
||||
container.push($('.navbar-collapse.collapse.in'));
|
||||
|
||||
$.each(container, function(key, value) {
|
||||
if (!$(value).is(e.target) // if the target of the click isn't the container...
|
||||
&& $(value).has(e.target).length === 0) // ... nor a descendant of the container
|
||||
{
|
||||
if ( $(value).hasClass('dropdown-menu') )
|
||||
{
|
||||
$(value).hide();
|
||||
} else
|
||||
{
|
||||
if ( $(value).hasClass('collapse') )
|
||||
{
|
||||
$(value).collapse('toggle');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Split path name to array and remove blanks
|
||||
url = window.location.pathname
|
||||
|
||||
// Move create shelf
|
||||
$( '#nav_createshelf' ).prependTo( '.your-shelves' );
|
||||
|
||||
// Create drop-down for profile and move elements to it
|
||||
$( '#main-nav' )
|
||||
.prepend( '<li class="dropdown"><a href="#" class="dropdown-toggle profileDrop" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-user"></span></a><ul class="dropdown-menu profileDropli"></ul></li>' );
|
||||
$( '#top_user' ).parent().addClass( 'dropdown' ).appendTo( '.profileDropli' );
|
||||
$( '#nav_about' ).addClass( 'dropdown' ).appendTo( '.profileDropli' );
|
||||
$( '#register' ).parent().addClass( 'dropdown' ).appendTo( '.profileDropli' );
|
||||
$( '#logout' ).parent().addClass( 'dropdown' ).appendTo( '.profileDropli' );
|
||||
|
||||
// Remove the modals except from some areas where they are needed
|
||||
bodyClass = $( 'body' ).attr( 'class' ).split(' ');
|
||||
modalWanted = ['admin', 'editbook', 'config', 'uiconfig'];
|
||||
|
||||
if ( $.inArray( bodyClass[0], modalWanted) != -1 ) {
|
||||
} else {
|
||||
$(' a:not(.dropdown-toggle) ')
|
||||
.removeAttr( 'data-toggle', 'data-target', 'data-remote' );
|
||||
}
|
||||
|
||||
|
||||
// Add classes to global buttons
|
||||
$( '#top_tasks' ).parent().addClass( 'top_tasks' );
|
||||
$( '#top_admin' ).parent().addClass( 'top_admin' );
|
||||
$( '#form-upload' ).parent().addClass( 'form-upload' );
|
||||
|
||||
// Search button work
|
||||
$( 'input#query' ).focus(function() {
|
||||
$( 'form[role="search"]' ).addClass( 'search-focus' );
|
||||
});
|
||||
$( 'input#query' ).focusout(function() {
|
||||
setTimeout(function() {
|
||||
$( 'form[role="search"]' ).removeClass( 'search-focus' );
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Check if dropdown goes out of viewport and add class
|
||||
|
||||
$(document).on('click','.dropdown-toggle',function() {
|
||||
// Add .offscreen if part of container not visible
|
||||
$('.dropdown-menu:visible').filter(function(){
|
||||
return $(this).visible() === false;
|
||||
}).each(function(){
|
||||
$(this).addClass('offscreen');
|
||||
});
|
||||
});
|
||||
|
||||
// Fade out content on page unload
|
||||
// delegate all clicks on "a" tag (links)
|
||||
/*$(document).on("click", "a:not(.btn-toolbar a, a[href*='shelf/remove'], .identifiers a, .bookinfo , .btn-group > a, #add-to-shelves a, #book-list a, .stat.blur a )", function () {
|
||||
|
||||
// get the href attribute
|
||||
var newUrl = $(this).attr("href");
|
||||
|
||||
// veryfy if the new url exists or is a hash
|
||||
if (!newUrl || newUrl[0] === "#") {
|
||||
// set that hash
|
||||
location.hash = newUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
now, fadeout the html (whole page)
|
||||
$( '.blur-wrapper' ).fadeOut(250);
|
||||
$(".row-fluid .col-sm-10").fadeOut(500,function () {
|
||||
// when the animation is complete, set the new location
|
||||
location = newUrl;
|
||||
});
|
||||
|
||||
// prevent the default browser behavior.
|
||||
return false;
|
||||
});*/
|
||||
|
||||
// Collapse long text into read-more
|
||||
$( 'div.comments' ).readmore( {
|
||||
collapsedHeight: 134,
|
||||
heightMargin: 45,
|
||||
speed: 300,
|
||||
moreLink: '<a href="#">READ MORE</a>', // ToDo: make translateable
|
||||
lessLink: '<a href="#">READ LESS</a>', // ToDo: make translateable
|
||||
});
|
||||
/////////////////////////////////
|
||||
// End of Global Work //
|
||||
///////////////////////////////
|
||||
|
||||
// Author Page Background Blur
|
||||
if ( $( 'body.author' ).length >0 ) {
|
||||
cover = $( '.author-bio img' ).attr( 'src' );
|
||||
$( '#loader + .container-fluid' )
|
||||
.prepend( '<div class="blur-wrapper"></div>' );
|
||||
$( '.blur-wrapper' ).prepend( '<img class="bg-blur" src="' + cover + '">' );
|
||||
// Place undefined cover images inside container
|
||||
if ( $( '.bg-blur[src="undefined"]' ).length > 0 ) {
|
||||
$( '.bg-blur' ).before( '<div class="bg-blur undefined-img"></div>' );
|
||||
$( 'img.bg-blur' ).appendTo( '.undefined-img' );
|
||||
}
|
||||
}
|
||||
|
||||
// Ereader Page - add class to iframe body on ereader page after it loads.
|
||||
backurl = '../../book/' + url[2]
|
||||
$( 'body.epub #title-controls' )
|
||||
.append('<div class="epub-back"><input action="action" onclick="location.href=backurl; return false;" type="button" value="Back" /></div>')
|
||||
|
||||
$( 'body.stat .col-sm-10 p:first' ).insertAfter( '#libs' );
|
||||
|
||||
// Check if link is external and force _blank attribute
|
||||
$(function(){ // document ready
|
||||
$( 'a' ).filter(function () {
|
||||
return this.hostname && this.hostname !== location.hostname;
|
||||
}).each(function () {
|
||||
$(this).addClass("external").attr( 'target', '_blank' );
|
||||
});
|
||||
});
|
||||
|
||||
// Check if lists are empty and add class to buttons
|
||||
if ( $.trim( $('#add-to-shelves').html() ).length === 0 ) {
|
||||
$( '#add-to-shelf' ).addClass( 'empty-ul' );
|
||||
}
|
||||
|
||||
shelfLength = $('#add-to-shelves li').length
|
||||
emptyLength = 0
|
||||
|
||||
$('#add-to-shelves').on('click','li a',function(){
|
||||
console.log('#remove-from-shelves change registered' );
|
||||
emptyLength++
|
||||
|
||||
setTimeout(function() {
|
||||
if ( emptyLength >= shelfLength ) {
|
||||
console.log('list is empty; adding empty-ul class' );
|
||||
$( '#add-to-shelf' ).addClass( 'empty-ul' );
|
||||
} else {
|
||||
console.log('list is not empty; removing empty-ul class' );
|
||||
$( '#add-to-shelf' ).removeClass( 'empty-ul' );
|
||||
}
|
||||
},100);
|
||||
});
|
||||
|
||||
if ( $.trim( $( 'ul[aria-labelledby="read-in-browser"] li' ).html() ).length === 0 ) {
|
||||
$('#read-in-browser').addClass('empty-ul');
|
||||
}
|
||||
|
||||
// Shelf Buttons and Tooltips
|
||||
if ( $( 'body.shelf' ).length > 0 ) {
|
||||
$( 'div[data-target="#DeleteShelfDialog"]' )
|
||||
.before( '<div class=".btn-group shelf-btn-group"></div>' )
|
||||
.appendTo( '.shelf-btn-group' )
|
||||
.addClass( 'delete-shelf-btn' );
|
||||
|
||||
$( 'a[href*="edit"]' )
|
||||
.appendTo( '.shelf-btn-group' )
|
||||
.addClass( 'edit-shelf-btn' );
|
||||
|
||||
$( 'a[href*="order"]' )
|
||||
.appendTo( '.shelf-btn-group' )
|
||||
.addClass( 'order-shelf-btn' );
|
||||
$( '.delete-shelf-btn' ).attr({
|
||||
'data-toggle-two': 'tooltip',
|
||||
'title': $( '.delete-shelf-btn' ).text(), // 'Delete Shelf'
|
||||
'data-placement': 'bottom' })
|
||||
.addClass('delete-btn-tooltip');
|
||||
|
||||
$( '.edit-shelf-btn' ).attr({
|
||||
'data-toggle-two': 'tooltip',
|
||||
'title': $( '.edit-shelf-btn' ).text(), // 'Edit Shelf'
|
||||
'data-placement': 'bottom' })
|
||||
.addClass('edit-btn-tooltip');
|
||||
|
||||
$( '.order-shelf-btn' ).attr({
|
||||
'data-toggle-two': 'tooltip',
|
||||
'title': $( '.order-shelf-btn' ).text(), //'Reorder Shelf'
|
||||
'data-placement': 'bottom' })
|
||||
.addClass('order-btn-tooltip');
|
||||
}
|
||||
|
||||
// Rest of Tooltips
|
||||
$( '.home-btn > a' ).attr({
|
||||
'data-toggle': 'tooltip',
|
||||
'title': $(document.body).attr('data-text'), // Home
|
||||
'data-placement': 'bottom' })
|
||||
.addClass('home-btn-tooltip');
|
||||
|
||||
$( '.plexBack > a' ).attr({
|
||||
'data-toggle': 'tooltip',
|
||||
'title': $(document.body).attr('data-textback'), // Back
|
||||
'data-placement': 'bottom' })
|
||||
.addClass('back-btn-tooltip');
|
||||
|
||||
$( '#top_tasks' ).attr({
|
||||
'data-toggle': 'tooltip',
|
||||
'title': $( '#top_tasks' ).text(), // 'Tasks'
|
||||
'data-placement': 'bottom',
|
||||
'data-viewport': '#main-nav' })
|
||||
.addClass('tasks-btn-tooltip');
|
||||
|
||||
$( '#top_admin' ).attr({
|
||||
'data-toggle': 'tooltip',
|
||||
'title': $( '#top_admin' ).attr('data-text'), // Settings
|
||||
'data-placement': 'bottom',
|
||||
'data-viewport': '#main-nav' })
|
||||
.addClass('admin-btn-tooltip');
|
||||
|
||||
$( '.profileDrop' ).attr({
|
||||
'title': $( '#top_user' ).attr('data-text'), //Account
|
||||
'data-placement': 'bottom',
|
||||
'data-toggle-two': 'tooltip',
|
||||
'data-viewport': '#main-nav' })
|
||||
.addClass('send-btn-tooltip dropdown');
|
||||
|
||||
$( '#btn-upload' ).attr({
|
||||
'data-toggle': 'tooltip',
|
||||
'title': $( '#btn-upload' ).parent().text() , // 'Upload'
|
||||
'data-placement': 'bottom',
|
||||
'data-viewport': '#main-nav' })
|
||||
.addClass('upload-btn-tooltip');
|
||||
|
||||
$( '#add-to-shelf' ).attr({
|
||||
'data-toggle-two': 'tooltip',
|
||||
'title': $( '#add-to-shelf' ).text() , // 'Add to Shelf'
|
||||
'data-placement': 'bottom',
|
||||
'data-viewport': '.btn-toolbar' })
|
||||
.addClass('addtoshelf-btn-tooltip');
|
||||
|
||||
$( '#have_read_cb' ).attr({
|
||||
'data-toggle': 'tooltip',
|
||||
'title': $( '#have_read_cb').attr('data-unchecked'),
|
||||
'data-placement': 'bottom',
|
||||
'data-viewport': '.btn-toolbar' })
|
||||
.addClass('readunread-btn-tooltip');
|
||||
|
||||
$( '#have_read_cb:checked' ).attr({
|
||||
'data-toggle': 'tooltip',
|
||||
'title': $( '#have_read_cb').attr('data-checked'),
|
||||
'data-placement': 'bottom',
|
||||
'data-viewport': '.btn-toolbar' })
|
||||
.addClass('readunread-btn-tooltip');
|
||||
|
||||
$( 'button#delete' ).attr({
|
||||
'data-toggle-two': 'tooltip',
|
||||
'title': $( 'button#delete' ).text(), //'Delete'
|
||||
'data-placement': 'bottom',
|
||||
'data-viewport': '.btn-toolbar' })
|
||||
.addClass('delete-book-btn-tooltip');
|
||||
|
||||
$( '#have_read_cb' ).click(function() {
|
||||
if ( $( '#have_read_cb:checked' ).length > 0 ) {
|
||||
$( this ).attr('data-original-title', $('#have_read_cb').attr('data-checked'));
|
||||
} else {
|
||||
$( this).attr('data-original-title', $('#have_read_cb').attr('data-unchecked'));
|
||||
}
|
||||
});
|
||||
|
||||
$( '.btn-group[aria-label="Edit/Delete book"] a' ).attr({
|
||||
'data-toggle': 'tooltip',
|
||||
'title': $( '#edit_book' ).text(), // 'Edit'
|
||||
'data-placement': 'bottom',
|
||||
'data-viewport': '.btn-toolbar' })
|
||||
.addClass('edit-btn-tooltip');
|
||||
|
||||
$( '#sendbtn' ).attr({
|
||||
'data-toggle': 'tooltip',
|
||||
'title': $( '#sendbtn' ).attr('data-text'),
|
||||
'data-placement': 'bottom',
|
||||
'data-viewport': '.btn-toolbar' })
|
||||
.addClass('send-btn-tooltip');
|
||||
|
||||
$( '#sendbtn2' ).attr({
|
||||
'data-toggle-two': 'tooltip',
|
||||
'title': $( '#sendbtn2' ).text(), // 'Send to Kindle',
|
||||
'data-placement': 'bottom',
|
||||
'data-viewport': '.btn-toolbar' })
|
||||
.addClass('send-btn-tooltip');
|
||||
|
||||
$( '#read-in-browser' ).attr({
|
||||
'data-toggle-two': 'tooltip',
|
||||
'title': $( '#read-in-browser' ).text(),
|
||||
'data-placement': 'bottom',
|
||||
'data-viewport': '.btn-toolbar'})
|
||||
.addClass('send-btn-tooltip');
|
||||
|
||||
$( '#btnGroupDrop1' ).attr({
|
||||
'data-toggle-two': 'tooltip',
|
||||
'title': $( '#btnGroupDrop1' ).text(),
|
||||
'data-placement': 'bottom',
|
||||
'data-viewport': '.btn-toolbar' });
|
||||
|
||||
if ( $( 'body.epub').length === 0 ) {
|
||||
$(document).ready(function(){
|
||||
$('[data-toggle="tooltip"]').tooltip({container: 'body', trigger: 'hover'});
|
||||
$('[data-toggle-two="tooltip"]').tooltip({container: 'body', trigger: 'hover'});
|
||||
$( '#btn-upload' ).attr('title', " ");
|
||||
});
|
||||
|
||||
|
||||
$( '[data-toggle-two="tooltip"]' ).click(function(){
|
||||
$('[data-toggle-two="tooltip"]').tooltip('hide');
|
||||
});
|
||||
|
||||
$( '[data-toggle="tooltip"]' ).click(function(){
|
||||
$('[data-toggle="tooltip"]').tooltip('hide');
|
||||
});
|
||||
}
|
||||
|
||||
$( '#read-in-browser a' ).attr('target',"");
|
||||
|
||||
if ( $( '.edit-shelf-btn').length > 1 ) {
|
||||
$( '.edit-shelf-btn:first').remove();
|
||||
}
|
||||
if ( $( '.order-shelf-btn').length > 1 ) {
|
||||
$( '.order-shelf-btn:first').remove();
|
||||
}
|
||||
|
||||
$( '#top_user > span.hidden-sm' ).clone().insertBefore( '.profileDropli' );
|
||||
$( '.navbar-collapse.collapse.in').before('<div class="sidebar-backdrop"></div>');
|
||||
|
||||
// Get rid of leading white space
|
||||
recentlyAdded = $( '#nav_new a:contains("Recently")' ).text().trim();
|
||||
$('#nav_new a:contains("Recently")').contents().filter(function() {
|
||||
return this.nodeType == 3
|
||||
}).each(function(){
|
||||
this.textContent = this.textContent.replace(' Recently Added',recentlyAdded);
|
||||
});
|
||||
|
||||
// Change shelf textValue
|
||||
shelfText = $( '.shelf .discover h2:first' ).text().replace(':',' —').replace(/\'/g,'');
|
||||
$( '.shelf .discover h2:first' ).text(shelfText);
|
||||
|
||||
shelfText = $( '.shelforder .col-sm-10 .col-sm-6.col-lg-6.col-xs-6 h2:first' ).text().replace(':',' —').replace(/\'/g,'');
|
||||
$( '.shelforder .col-sm-10 .col-sm-6.col-lg-6.col-xs-6 h2:first' ).text(shelfText);
|
||||
|
||||
|
||||
function mobileSupport() {
|
||||
if ( $( window ).width() <= 768 ) {
|
||||
//Move menu to collapse
|
||||
$( '.row-fluid > .col-sm-2:first' ).appendTo( '.navbar-collapse.collapse:first');
|
||||
if ( $( '.sidebar-backdrop' ).length < 1 ) {
|
||||
$( '.navbar-collapse.collapse:first' ).after( '<div class="sidebar-backdrop"></div>' );
|
||||
}
|
||||
} else {
|
||||
//Move menu out of collapse
|
||||
$( '.col-sm-2:first' ).insertBefore( '.col-sm-10:first');
|
||||
$( '.sidebar-backdrop' ).remove();
|
||||
}
|
||||
}
|
||||
|
||||
// LayerCake plug
|
||||
if ( $(' body.stat p').length > 0 ) {
|
||||
$(' body.stat p').append(" and <a href='https://github.com/leram84/layer.Cake/tree/master/caliBlur' target='_blank'>layer.Cake</a>");
|
||||
str = $(' body.stat p').html().replace("</a>.","</a>");
|
||||
$(' body.stat p').html(str);
|
||||
}
|
||||
// Collect delete buttons in editbook to single dropdown
|
||||
$( '.editbook .text-center.more-stuff' ).prepend( '<button id="deleteButton" type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-remove"></span>Delete Format<span class="caret"></span></button><ul class="dropdown-menu delete-dropdown"></ul>' );
|
||||
|
||||
deleteButtons = $( '.editbook .text-center.more-stuff a' ).removeClass('btn btn-danger' ).attr( 'type', '').get();
|
||||
|
||||
$( deleteButtons ).detach();
|
||||
$( '.editbook .text-center.more-stuff h4' ).remove();
|
||||
$.each(deleteButtons, function(i, val) {
|
||||
$( '<li>' + deleteButtons[i].outerHTML + '</li>' ).appendTo( '.delete-dropdown' );
|
||||
});
|
||||
|
||||
// Turn off bootstrap animations
|
||||
$(function() { $.support.transition = false; })
|
||||
|
||||
mobileSupport();
|
||||
|
||||
// Only call function once resize is complete
|
||||
//var id;
|
||||
$( window ).on('resize',function() {
|
||||
// clearTimeout(id);
|
||||
// id = setTimeout(mobileSupport, 500);
|
||||
mobileSupport();
|
||||
});
|
@ -1,3 +1,20 @@
|
||||
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
* Copyright (C) 2018 jkrehm
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/* global _ */
|
||||
|
||||
$(function() {
|
||||
|
@ -142,6 +142,17 @@ var languages = new Bloodhound({
|
||||
}
|
||||
});
|
||||
|
||||
var publishers = new Bloodhound({
|
||||
name: "publisher",
|
||||
datumTokenizer: function datumTokenizer(datum) {
|
||||
return [datum.name];
|
||||
},
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||
remote: {
|
||||
url: getPath() + "/get_publishers_json?q=%QUERY"
|
||||
}
|
||||
});
|
||||
|
||||
function sourceSplit(query, cb, split, source) {
|
||||
var bhAdapter = source.ttAdapter();
|
||||
|
||||
@ -224,6 +235,20 @@ promiseLanguages.done(function() {
|
||||
);
|
||||
});
|
||||
|
||||
var promisePublishers = publishers.initialize();
|
||||
promisePublishers.done(function() {
|
||||
$("#publisher").typeahead(
|
||||
{
|
||||
highlight: true, minLength: 0,
|
||||
hint: true
|
||||
}, {
|
||||
name: "publishers",
|
||||
displayKey: "name",
|
||||
source: publishers.ttAdapter()
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#search").on("change input.typeahead:selected", function() {
|
||||
var form = $("form").serialize();
|
||||
$.getJSON( getPath() + "/get_matching_tags", form, function( data ) {
|
||||
|
@ -1,6 +1,21 @@
|
||||
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
* Copyright (C) 2018 idalin<dalin.lin@gmail.com>
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
/*
|
||||
* Get Metadata from Douban Books api and Google Books api
|
||||
* Created by idalin<dalin.lin@gmail.com>
|
||||
* Google Books api document: https://developers.google.com/books/docs/v1/using
|
||||
* Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
|
||||
*/
|
||||
|
@ -99,14 +99,15 @@ kthoom.setSettings = function() {
|
||||
};
|
||||
|
||||
var createURLFromArray = function(array, mimeType) {
|
||||
var offset = array.byteOffset, len = array.byteLength;
|
||||
var offset = array.byteOffset;
|
||||
var len = array.byteLength;
|
||||
var url;
|
||||
var blob;
|
||||
|
||||
if (mimeType === 'image/xml+svg') {
|
||||
const xmlStr = new TextDecoder('utf-8').decode(array);
|
||||
return 'data:image/svg+xml;UTF-8,' + encodeURIComponent(xmlStr);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move all this browser support testing to a common place
|
||||
// and do it just once.
|
||||
@ -137,11 +138,13 @@ var createURLFromArray = function(array, mimeType) {
|
||||
kthoom.ImageFile = function(file) {
|
||||
this.filename = file.filename;
|
||||
var fileExtension = file.filename.split(".").pop().toLowerCase();
|
||||
var mimeType = fileExtension === "png" ? "image/png" :
|
||||
this.mimeType = fileExtension === "png" ? "image/png" :
|
||||
(fileExtension === "jpg" || fileExtension === "jpeg") ? "image/jpeg" :
|
||||
fileExtension === "gif" ? "image/gif" : fileExtension == 'svg' ? 'image/xml+svg' : undefined;
|
||||
this.dataURI = createURLFromArray(file.fileData, mimeType);
|
||||
this.data = file;
|
||||
if ( this.mimeType !== undefined) {
|
||||
this.dataURI = createURLFromArray(file.fileData, this.mimeType);
|
||||
this.data = file;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -169,34 +172,42 @@ function loadFromArrayBuffer(ab) {
|
||||
unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.PROGRESS,
|
||||
function(e) {
|
||||
var percentage = e.currentBytesUnarchived / e.totalUncompressedBytesInArchive;
|
||||
totalImages = e.totalFilesInArchive;
|
||||
if (totalImages === 0) {
|
||||
totalImages = e.totalFilesInArchive;
|
||||
}
|
||||
updateProgress(percentage *100);
|
||||
lastCompletion = percentage * 100;
|
||||
});
|
||||
unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.EXTRACT,
|
||||
function(e) {
|
||||
// convert DecompressedFile into a bunch of ImageFiles
|
||||
// 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) {
|
||||
imageFilenames.push(f.filename);
|
||||
imageFiles.push(new kthoom.ImageFile(f));
|
||||
// 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>"
|
||||
);
|
||||
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 + "'>" +
|
||||
"<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);
|
||||
}
|
||||
}
|
||||
else {
|
||||
totalImages--;
|
||||
}
|
||||
}
|
||||
}
|
||||
// display first page if we haven't yet
|
||||
if (imageFiles.length === currentImage + 1) {
|
||||
updatePage(lastCompletion);
|
||||
}
|
||||
});
|
||||
unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.FINISH,
|
||||
function() {
|
||||
|
File diff suppressed because one or more lines are too long
1
cps/static/js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ja.min.js
vendored
Normal file
1
cps/static/js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ja.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.ja={days:["日曜","月曜","火曜","水曜","木曜","金曜","土曜"],daysShort:["日","月","火","水","木","金","土"],daysMin:["日","月","火","水","木","金","土"],months:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],today:"今日",format:"yyyy/mm/dd",titleFormat:"yyyy年mm月",clear:"クリア"}}(jQuery);
|
7
cps/static/js/libs/bootstrap.min.js
vendored
7
cps/static/js/libs/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
6
cps/static/js/libs/compromise.min.js
vendored
Normal file
6
cps/static/js/libs/compromise.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
cps/static/js/libs/jquery.min.js
vendored
7
cps/static/js/libs/jquery.min.js
vendored
File diff suppressed because one or more lines are too long
2
cps/static/js/libs/jquery.min.map
vendored
2
cps/static/js/libs/jquery.min.map
vendored
File diff suppressed because one or more lines are too long
1
cps/static/js/libs/jquery.visible.min.js
vendored
Normal file
1
cps/static/js/libs/jquery.visible.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
!function(t){var i=t(window);t.fn.visible=function(t,e,o){if(!(this.length<1)){var r=this.length>1?this.eq(0):this,n=r.get(0),f=i.width(),h=i.height(),o=o?o:"both",l=e===!0?n.offsetWidth*n.offsetHeight:!0;if("function"==typeof n.getBoundingClientRect){var g=n.getBoundingClientRect(),u=g.top>=0&&g.top<h,s=g.bottom>0&&g.bottom<=h,c=g.left>=0&&g.left<f,a=g.right>0&&g.right<=f,v=t?u||s:u&&s,b=t?c||a:c&&a;if("both"===o)return l&&v&&b;if("vertical"===o)return l&&v;if("horizontal"===o)return l&&b}else{var d=i.scrollTop(),p=d+h,w=i.scrollLeft(),m=w+f,y=r.offset(),z=y.top,B=z+r.height(),C=y.left,R=C+r.width(),j=t===!0?B:z,q=t===!0?z:B,H=t===!0?R:C,L=t===!0?C:R;if("both"===o)return!!l&&p>=q&&j>=d&&m>=L&&H>=w;if("vertical"===o)return!!l&&p>=q&&j>=d;if("horizontal"===o)return!!l&&m>=L&&H>=w}}}}(jQuery);
|
70
cps/static/js/libs/plugins.js
vendored
70
cps/static/js/libs/plugins.js
vendored
File diff suppressed because one or more lines are too long
11
cps/static/js/libs/readmore.min.js
vendored
Normal file
11
cps/static/js/libs/readmore.min.js
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
/*!
|
||||
* @preserve
|
||||
*
|
||||
* Readmore.js jQuery plugin
|
||||
* Author: @jed_foster
|
||||
* Project home: http://jedfoster.github.io/Readmore.js
|
||||
* Licensed under the MIT license
|
||||
*
|
||||
* Debounce function from http://davidwalsh.name/javascript-debounce-function
|
||||
*/
|
||||
!function(t){"function"==typeof define&&define.amd?define(["jquery"],t):"object"==typeof exports?module.exports=t(require("jquery")):t(jQuery)}(function(t){"use strict";function e(t,e,i){var o;return function(){var n=this,a=arguments,s=function(){o=null,i||t.apply(n,a)},r=i&&!o;clearTimeout(o),o=setTimeout(s,e),r&&t.apply(n,a)}}function i(t){var e=++h;return String(null==t?"rmjs-":t)+e}function o(t){var e=t.clone().css({height:"auto",width:t.width(),maxHeight:"none",overflow:"hidden"}).insertAfter(t),i=e.outerHeight(),o=parseInt(e.css({maxHeight:""}).css("max-height").replace(/[^-\d\.]/g,""),10),n=t.data("defaultHeight");e.remove();var a=o||t.data("collapsedHeight")||n;t.data({expandedHeight:i,maxHeight:o,collapsedHeight:a}).css({maxHeight:"none"})}function n(t){if(!d[t.selector]){var e=" ";t.embedCSS&&""!==t.blockCSS&&(e+=t.selector+" + [data-readmore-toggle], "+t.selector+"[data-readmore]{"+t.blockCSS+"}"),e+=t.selector+"[data-readmore]{transition: height "+t.speed+"ms;overflow: hidden;}",function(t,e){var i=t.createElement("style");i.type="text/css",i.styleSheet?i.styleSheet.cssText=e:i.appendChild(t.createTextNode(e)),t.getElementsByTagName("head")[0].appendChild(i)}(document,e),d[t.selector]=!0}}function a(e,i){this.element=e,this.options=t.extend({},r,i),n(this.options),this._defaults=r,this._name=s,this.init(),window.addEventListener?(window.addEventListener("load",c),window.addEventListener("resize",c)):(window.attachEvent("load",c),window.attachEvent("resize",c))}var s="readmore",r={speed:100,collapsedHeight:200,heightMargin:16,moreLink:'<a href="#">Read More</a>',lessLink:'<a href="#">Close</a>',embedCSS:!0,blockCSS:"display: block; width: 100%;",startOpen:!1,blockProcessed:function(){},beforeToggle:function(){},afterToggle:function(){}},d={},h=0,c=e(function(){t("[data-readmore]").each(function(){var e=t(this),i="true"===e.attr("aria-expanded");o(e),e.css({height:e.data(i?"expandedHeight":"collapsedHeight")})})},100);a.prototype={init:function(){var e=t(this.element);e.data({defaultHeight:this.options.collapsedHeight,heightMargin:this.options.heightMargin}),o(e);var n=e.data("collapsedHeight"),a=e.data("heightMargin");if(e.outerHeight(!0)<=n+a)return this.options.blockProcessed&&"function"==typeof this.options.blockProcessed&&this.options.blockProcessed(e,!1),!0;var s=e.attr("id")||i(),r=this.options.startOpen?this.options.lessLink:this.options.moreLink;e.attr({"data-readmore":"","aria-expanded":this.options.startOpen,id:s}),e.after(t(r).on("click",function(t){return function(i){t.toggle(this,e[0],i)}}(this)).attr({"data-readmore-toggle":s,"aria-controls":s})),this.options.startOpen||e.css({height:n}),this.options.blockProcessed&&"function"==typeof this.options.blockProcessed&&this.options.blockProcessed(e,!0)},toggle:function(e,i,o){o&&o.preventDefault(),e||(e=t('[aria-controls="'+this.element.id+'"]')[0]),i||(i=this.element);var n=t(i),a="",s="",r=!1,d=n.data("collapsedHeight");n.height()<=d?(a=n.data("expandedHeight")+"px",s="lessLink",r=!0):(a=d,s="moreLink"),this.options.beforeToggle&&"function"==typeof this.options.beforeToggle&&this.options.beforeToggle(e,n,!r),n.css({height:a}),n.on("transitionend",function(i){return function(){i.options.afterToggle&&"function"==typeof i.options.afterToggle&&i.options.afterToggle(e,n,r),t(this).attr({"aria-expanded":r}).off("transitionend")}}(this)),t(e).replaceWith(t(this.options[s]).on("click",function(t){return function(e){t.toggle(this,i,e)}}(this)).attr({"data-readmore-toggle":n.attr("id"),"aria-controls":n.attr("id")}))},destroy:function(){t(this.element).each(function(){var e=t(this);e.attr({"data-readmore":null,"aria-expanded":null}).css({maxHeight:"",height:""}).next("[data-readmore-toggle]").remove(),e.removeData()})}},t.fn.readmore=function(e){var i=arguments,o=this.selector;return e=e||{},"object"==typeof e?this.each(function(){if(t.data(this,"plugin_"+s)){var i=t.data(this,"plugin_"+s);i.destroy.apply(i)}e.selector=o,t.data(this,"plugin_"+s,new a(this,e))}):"string"==typeof e&&"_"!==e[0]&&"init"!==e?this.each(function(){var o=t.data(this,"plugin_"+s);o instanceof a&&"function"==typeof o[e]&&o[e].apply(o,Array.prototype.slice.call(i,1))}):void 0}});
|
7
cps/static/js/libs/underscore-min.js
vendored
7
cps/static/js/libs/underscore-min.js
vendored
File diff suppressed because one or more lines are too long
2
cps/static/js/libs/underscore-min.map
vendored
2
cps/static/js/libs/underscore-min.map
vendored
File diff suppressed because one or more lines are too long
@ -1,3 +1,20 @@
|
||||
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
* Copyright (C) 2012-2019 mutschler, janeczku, jkrehm, 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/>.
|
||||
*/
|
||||
|
||||
// Generic control/related handler to show/hide fields based on a checkbox' value
|
||||
// e.g.
|
||||
// <input type="checkbox" data-control="stuff-to-show">
|
||||
@ -60,25 +77,20 @@ $(function() {
|
||||
layoutMode : "fitRows"
|
||||
});
|
||||
|
||||
$(".load-more .row").infinitescroll({
|
||||
var $loadMore = $(".load-more .row").infiniteScroll({
|
||||
debug: false,
|
||||
navSelector : ".pagination",
|
||||
// selector for the paged navigation (it will be hidden)
|
||||
nextSelector : ".pagination a:last",
|
||||
path : ".next",
|
||||
// selector for the NEXT link (to page 2)
|
||||
itemSelector : ".load-more .book",
|
||||
animate : true,
|
||||
extraScrollPx: 300
|
||||
// selector for all items you'll retrieve
|
||||
}, function(data) {
|
||||
append : ".load-more .book"
|
||||
//animate : true, # ToDo: Reenable function
|
||||
//extraScrollPx: 300
|
||||
});
|
||||
$loadMore.on( "append.infiniteScroll", function( event, response, path, data ) {
|
||||
$(".pagination").addClass("hidden");
|
||||
$(".load-more .row").isotope( "appended", $(data), null );
|
||||
});
|
||||
|
||||
$("#sendbtn").click(function() {
|
||||
var $this = $(this);
|
||||
$this.text("Please wait...");
|
||||
$this.addClass("disabled");
|
||||
});
|
||||
$("#restart").click(function() {
|
||||
$.ajax({
|
||||
dataType: "json",
|
||||
@ -104,15 +116,18 @@ $(function() {
|
||||
var $this = $(this);
|
||||
var buttonText = $this.html();
|
||||
$this.html("...");
|
||||
$("#update_error").addClass("hidden")
|
||||
$("#update_error").addClass("hidden");
|
||||
if ($("#message").length) {
|
||||
$("#message").alert("close");
|
||||
}
|
||||
$.ajax({
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../../get_update_status",
|
||||
success: function success(data) {
|
||||
$this.html(buttonText);
|
||||
|
||||
var cssClass = '';
|
||||
var message = ''
|
||||
var cssClass = "";
|
||||
var message = "";
|
||||
|
||||
if (data.success === true) {
|
||||
if (data.update === true) {
|
||||
@ -122,19 +137,20 @@ $(function() {
|
||||
.removeClass("hidden")
|
||||
.find("span").html(data.commit);
|
||||
|
||||
data.history.reverse().forEach((entry, index) => {
|
||||
data.history.forEach(function(entry) {
|
||||
$("<tr><td>" + entry[0] + "</td><td>" + entry[1] + "</td></tr>").appendTo($("#update_table"));
|
||||
});
|
||||
cssClass = 'alert-warning'
|
||||
cssClass = "alert-warning";
|
||||
} else {
|
||||
cssClass = 'alert-success'
|
||||
cssClass = "alert-success";
|
||||
}
|
||||
} else {
|
||||
cssClass = 'alert-danger'
|
||||
cssClass = "alert-danger";
|
||||
}
|
||||
|
||||
message = '<div class="alert ' + cssClass
|
||||
+ ' fade in"><a href="#" class="close" data-dismiss="alert">×</a>' + data.message + '</div>';
|
||||
message = "<div id=\"message\" class=\"alert " + cssClass
|
||||
+ " fade in\"><a href=\"#\" class=\"close\" data-dismiss=\"alert\">×</a>"
|
||||
+ data.message + "</div>";
|
||||
|
||||
$(message).insertAfter($("#update_table"));
|
||||
}
|
||||
@ -163,6 +179,7 @@ $(function() {
|
||||
});
|
||||
});
|
||||
|
||||
// Init all data control handlers to default
|
||||
$("input[data-control]").trigger("change");
|
||||
|
||||
$("#bookDetailsModal")
|
||||
@ -186,6 +203,14 @@ $(function() {
|
||||
});
|
||||
|
||||
$(window).resize(function() {
|
||||
$(".discover .row").isotope("reLayout");
|
||||
$(".discover .row").isotope("layout");
|
||||
});
|
||||
|
||||
$(".author-expand").click(function() {
|
||||
$(this).parent().find("a.author-name").slice($(this).data("authors-max")).toggle();
|
||||
$(this).parent().find("span.author-hidden-divider").toggle();
|
||||
$(this).html() === $(this).data("collapse-caption") ? $(this).html("(...)") : $(this).html($(this).data("collapse-caption"));
|
||||
$(".discover .row").isotope("layout");
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,3 +1,20 @@
|
||||
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
* Copyright (C) 2018 jkrehm, 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/>.
|
||||
*/
|
||||
|
||||
/* global Sortable,sortTrue */
|
||||
|
||||
Sortable.create(sortTrue, {
|
||||
|
@ -1,6 +1,23 @@
|
||||
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
* Copyright (C) 2018 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() {
|
||||
|
||||
$("#domain_submit").click(function(event){
|
||||
$("#domain_submit").click(function(event) {
|
||||
event.preventDefault();
|
||||
$("#domain_add").ajaxForm();
|
||||
$(this).closest("form").submit();
|
||||
@ -10,48 +27,49 @@ $(function() {
|
||||
async: true,
|
||||
timeout: 900,
|
||||
success:function(data){
|
||||
$('#domain-table').bootstrapTable("load", data);
|
||||
$("#domain-table").bootstrapTable("load", data);
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#domain-table').bootstrapTable({
|
||||
formatNoMatches: function () {
|
||||
return '';
|
||||
$("#domain-table").bootstrapTable({
|
||||
formatNoMatches: function () {
|
||||
return "";
|
||||
},
|
||||
striped: false
|
||||
});
|
||||
$("#btndeletedomain").click(function() {
|
||||
//get data-id attribute of the clicked element
|
||||
var domainId = $(this).data('domainId');
|
||||
var domainId = $(this).data("domainId");
|
||||
$.ajax({
|
||||
method:"post",
|
||||
url: window.location.pathname + "/../../ajax/deletedomain",
|
||||
data: {"domainid":domainId}
|
||||
});
|
||||
$('#DeleteDomain').modal('hide');
|
||||
$("#DeleteDomain").modal("hide");
|
||||
$.ajax({
|
||||
method:"get",
|
||||
url: window.location.pathname + "/../../ajax/domainlist",
|
||||
async: true,
|
||||
timeout: 900,
|
||||
success:function(data){
|
||||
$('#domain-table').bootstrapTable("load", data);
|
||||
success:function(data) {
|
||||
$("#domain-table").bootstrapTable("load", data);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
//triggered when modal is about to be shown
|
||||
$('#DeleteDomain').on('show.bs.modal', function(e) {
|
||||
$("#DeleteDomain").on("show.bs.modal", function(e) {
|
||||
//get data-id attribute of the clicked element and store in button
|
||||
var domainId = $(e.relatedTarget).data('domain-id');
|
||||
$(e.currentTarget).find("#btndeletedomain").data('domainId',domainId);
|
||||
var domainId = $(e.relatedTarget).data("domain-id");
|
||||
$(e.currentTarget).find("#btndeletedomain").data("domainId", domainId);
|
||||
});
|
||||
});
|
||||
|
||||
function TableActions (value, row, index) {
|
||||
/*function TableActions (value, row, index) {
|
||||
return [
|
||||
'<a class="danger remove" data-toggle="modal" data-target="#DeleteDomain" data-domain-id="'+row.id+'" title="Remove">',
|
||||
'<i class="glyphicon glyphicon-trash"></i>',
|
||||
'</a>'
|
||||
].join('');
|
||||
}
|
||||
"<a class=\"danger remove\" data-toggle=\"modal\" data-target=\"#DeleteDomain\" data-domain-id=\"" + row.id
|
||||
+ "\" title=\"Remove\">",
|
||||
"<i class=\"glyphicon glyphicon-trash\"></i>",
|
||||
"</a>"
|
||||
].join("");
|
||||
}*/
|
||||
|
@ -79,7 +79,7 @@ var RarVolumeHeader = function(bstream) {
|
||||
// bytes 4,5
|
||||
this.flags = {};
|
||||
this.flags.value = bstream.peekBits(16);
|
||||
|
||||
|
||||
info(" flags=" + twoByteValueToHexString(this.flags.value));
|
||||
switch (this.headType) {
|
||||
case MAIN_HEAD:
|
||||
@ -115,7 +115,7 @@ var RarVolumeHeader = function(bstream) {
|
||||
default:
|
||||
bstream.readBits(16);
|
||||
}
|
||||
|
||||
|
||||
// byte 6,7
|
||||
this.headSize = bstream.readBits(16);
|
||||
info(" headSize=" + this.headSize);
|
||||
@ -212,12 +212,12 @@ var RarVolumeHeader = function(bstream) {
|
||||
|
||||
//var BLOCK_LZ = 0;
|
||||
|
||||
var rLDecode = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224],
|
||||
var rLDecode = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224],
|
||||
rLBits = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5],
|
||||
rDBitLengthCounts = [4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 14, 0, 12],
|
||||
rSDDecode = [0, 4, 8, 16, 32, 64, 128, 192],
|
||||
rSDBits = [2, 2, 3, 4, 5, 6, 6, 6];
|
||||
|
||||
|
||||
var rDDecode = [0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32,
|
||||
48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072,
|
||||
4096, 6144, 8192, 12288, 16384, 24576, 32768, 49152, 65536, 98304,
|
||||
@ -269,18 +269,18 @@ var RD = { //rep decode
|
||||
var rBuffer;
|
||||
|
||||
// read in Huffman tables for RAR
|
||||
function RarReadTables(bstream) {
|
||||
function rarReadTables(bstream) {
|
||||
var BitLength = new Array(rBC),
|
||||
Table = new Array(rHuffTableSize);
|
||||
var i;
|
||||
// before we start anything we need to get byte-aligned
|
||||
bstream.readBits( (8 - bstream.bitPtr) & 0x7 );
|
||||
|
||||
|
||||
if (bstream.readBits(1)) {
|
||||
info("Error! PPM not implemented yet");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!bstream.readBits(1)) { //discard old table
|
||||
for (i = UnpOldTable.length; i--;) UnpOldTable[i] = 0;
|
||||
}
|
||||
@ -308,7 +308,7 @@ function RarReadTables(bstream) {
|
||||
// now all 20 bit lengths are obtained, we construct the Huffman Table:
|
||||
|
||||
rarMakeDecodeTables(BitLength, 0, BD, rBC);
|
||||
|
||||
|
||||
var TableSize = rHuffTableSize;
|
||||
//console.log(DecodeLen, DecodePos, DecodeNum);
|
||||
for (i = 0; i < TableSize;) {
|
||||
@ -332,12 +332,12 @@ function RarReadTables(bstream) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
rarMakeDecodeTables(Table, 0, LD, rNC);
|
||||
rarMakeDecodeTables(Table, rNC, DD, rDC);
|
||||
rarMakeDecodeTables(Table, rNC + rDC, LDD, rLDC);
|
||||
rarMakeDecodeTables(Table, rNC + rDC + rLDC, RD, rRC);
|
||||
|
||||
|
||||
for (i = UnpOldTable.length; i--;) {
|
||||
UnpOldTable[i] = Table[i];
|
||||
}
|
||||
@ -366,7 +366,7 @@ function rarDecodeNumber(bstream, dec) {
|
||||
: 15));
|
||||
bstream.readBits(bits);
|
||||
var N = DecodePos[bits] + ((bitField - DecodeLen[bits - 1]) >>> (16 - bits));
|
||||
|
||||
|
||||
return DecodeNum[N];
|
||||
}
|
||||
|
||||
@ -480,7 +480,7 @@ function Unpack20(bstream) { //, Solid) {
|
||||
continue;
|
||||
}
|
||||
if (num < 270) {
|
||||
var Distance = rSDDecode[num -= 261] + 1;
|
||||
Distance = rSDDecode[num -= 261] + 1;
|
||||
if ((Bits = rSDBits[num]) > 0) {
|
||||
Distance += bstream.readBits(Bits);
|
||||
}
|
||||
@ -513,9 +513,9 @@ function rarReadTables20(bstream) {
|
||||
var BitLength = new Array(rBC20);
|
||||
var Table = new Array(rMC20 * 4);
|
||||
var TableSize, N, I;
|
||||
var i;
|
||||
bstream.readBits(1);
|
||||
if (!bstream.readBits(1)) {
|
||||
var i;
|
||||
for (i = UnpOldTable20.length; i--;) UnpOldTable20[i] = 0;
|
||||
}
|
||||
TableSize = rNC20 + rDC20 + rRC20;
|
||||
@ -553,48 +553,49 @@ function rarReadTables20(bstream) {
|
||||
}
|
||||
|
||||
|
||||
function Unpack29(bstream, Solid) {
|
||||
function Unpack29(bstream) {
|
||||
// lazy initialize rDDecode and rDBits
|
||||
|
||||
var DDecode = new Array(rDC);
|
||||
var DBits = new Array(rDC);
|
||||
|
||||
var Distance = 0;
|
||||
var Length = 0;
|
||||
var Dist = 0, BitLength = 0, Slot = 0;
|
||||
var I;
|
||||
for (I = 0; I < rDBitLengthCounts.length; I++,BitLength++) {
|
||||
for (var J = 0; J < rDBitLengthCounts[I]; J++,Slot++,Dist+=(1<<BitLength)) {
|
||||
DDecode[Slot]=Dist;
|
||||
DBits[Slot]=BitLength;
|
||||
for (I = 0; I < rDBitLengthCounts.length; I++, BitLength++) {
|
||||
for (var J = 0; J < rDBitLengthCounts[I]; J++, Slot++, Dist += (1 << BitLength)) {
|
||||
DDecode[Slot] = Dist;
|
||||
DBits[Slot] = BitLength;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var Bits;
|
||||
//tablesRead = false;
|
||||
|
||||
rOldDist = [0, 0, 0, 0]
|
||||
rOldDist = [0, 0, 0, 0];
|
||||
|
||||
lastDist = 0;
|
||||
lastLength = 0;
|
||||
var i;
|
||||
for (i = UnpOldTable.length; i--;) UnpOldTable[i] = 0;
|
||||
|
||||
|
||||
// read in Huffman tables
|
||||
RarReadTables(bstream);
|
||||
|
||||
rarReadTables(bstream);
|
||||
|
||||
while (true) {
|
||||
var num = rarDecodeNumber(bstream, LD);
|
||||
|
||||
|
||||
if (num < 256) {
|
||||
rBuffer.insertByte(num);
|
||||
continue;
|
||||
}
|
||||
if (num >= 271) {
|
||||
var Length = rLDecode[num -= 271] + 3;
|
||||
Length = rLDecode[num -= 271] + 3;
|
||||
if ((Bits = rLBits[num]) > 0) {
|
||||
Length += bstream.readBits(Bits);
|
||||
}
|
||||
var DistNumber = rarDecodeNumber(bstream, DD);
|
||||
var Distance = DDecode[DistNumber]+1;
|
||||
Distance = DDecode[DistNumber] + 1;
|
||||
if ((Bits = DBits[DistNumber]) > 0) {
|
||||
if (DistNumber > 9) {
|
||||
if (Bits > 4) {
|
||||
@ -625,19 +626,19 @@ function Unpack29(bstream, Solid) {
|
||||
Length++;
|
||||
}
|
||||
}
|
||||
RarInsertOldDist(Distance);
|
||||
RarInsertLastMatch(Length, Distance);
|
||||
rarInsertOldDist(Distance);
|
||||
rarInsertLastMatch(Length, Distance);
|
||||
rarCopyString(Length, Distance);
|
||||
continue;
|
||||
}
|
||||
if (num === 256) {
|
||||
if (!RarReadEndOfBlock(bstream)) break;
|
||||
if (!rarReadEndOfBlock(bstream)) break;
|
||||
continue;
|
||||
}
|
||||
if (num === 257) {
|
||||
//console.log("READVMCODE");
|
||||
if (!RarReadVMCode(bstream)) break;
|
||||
continue;
|
||||
if (!rarReadVMCode(bstream)) break;
|
||||
continue;
|
||||
}
|
||||
if (num === 258) {
|
||||
if (lastLength != 0) {
|
||||
@ -647,39 +648,39 @@ function Unpack29(bstream, Solid) {
|
||||
}
|
||||
if (num < 263) {
|
||||
var DistNum = num - 259;
|
||||
var Distance = rOldDist[DistNum];
|
||||
Distance = rOldDist[DistNum];
|
||||
|
||||
for (var I = DistNum; I > 0; I--) {
|
||||
rOldDist[I] = rOldDist[I-1];
|
||||
rOldDist[I] = rOldDist[I - 1];
|
||||
}
|
||||
rOldDist[0] = Distance;
|
||||
|
||||
var LengthNumber = rarDecodeNumber(bstream, RD);
|
||||
var Length = rLDecode[LengthNumber] + 2;
|
||||
Length = rLDecode[LengthNumber] + 2;
|
||||
if ((Bits = rLBits[LengthNumber]) > 0) {
|
||||
Length += bstream.readBits(Bits);
|
||||
}
|
||||
RarInsertLastMatch(Length, Distance);
|
||||
rarInsertLastMatch(Length, Distance);
|
||||
rarCopyString(Length, Distance);
|
||||
continue;
|
||||
}
|
||||
if (num < 272) {
|
||||
var Distance = rSDDecode[num -= 263] + 1;
|
||||
Distance = rSDDecode[num -= 263] + 1;
|
||||
if ((Bits = rSDBits[num]) > 0) {
|
||||
Distance += bstream.readBits(Bits);
|
||||
}
|
||||
RarInsertOldDist(Distance);
|
||||
RarInsertLastMatch(2, Distance);
|
||||
rarInsertOldDist(Distance);
|
||||
rarInsertLastMatch(2, Distance);
|
||||
rarCopyString(2, Distance);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
rarUpdateProgress()
|
||||
rarUpdateProgress();
|
||||
}
|
||||
|
||||
function RarReadEndOfBlock(bstream) {
|
||||
|
||||
rarUpdateProgress()
|
||||
function rarReadEndOfBlock(bstream) {
|
||||
|
||||
rarUpdateProgress();
|
||||
|
||||
var NewTable = false, NewFile = false;
|
||||
if (bstream.readBits(1)) {
|
||||
@ -689,11 +690,11 @@ function RarReadEndOfBlock(bstream) {
|
||||
NewTable = !!bstream.readBits(1);
|
||||
}
|
||||
//tablesRead = !NewTable;
|
||||
return !(NewFile || NewTable && !RarReadTables(bstream));
|
||||
return !(NewFile || NewTable && !rarReadTables(bstream));
|
||||
}
|
||||
|
||||
|
||||
function RarReadVMCode(bstream) {
|
||||
function rarReadVMCode(bstream) {
|
||||
var FirstByte = bstream.readBits(8);
|
||||
var Length = (FirstByte & 7) + 1;
|
||||
if (Length === 7) {
|
||||
@ -702,7 +703,7 @@ function RarReadVMCode(bstream) {
|
||||
Length = bstream.readBits(16);
|
||||
}
|
||||
var vmCode = [];
|
||||
for(var I = 0; I < Length; I++) {
|
||||
for (var I = 0; I < Length; I++) {
|
||||
//do something here with cheking readbuf
|
||||
vmCode.push(bstream.readBits(8));
|
||||
}
|
||||
@ -717,14 +718,14 @@ function RarAddVMCode(firstByte, vmCode, length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function RarInsertLastMatch(length, distance) {
|
||||
function rarInsertLastMatch(length, distance) {
|
||||
lastDist = distance;
|
||||
lastLength = length;
|
||||
}
|
||||
|
||||
function RarInsertOldDist(distance) {
|
||||
rOldDist.splice(3,1);
|
||||
rOldDist.splice(0,0,distance);
|
||||
function rarInsertOldDist(distance) {
|
||||
rOldDist.splice(3, 1);
|
||||
rOldDist.splice(0, 0, distance);
|
||||
}
|
||||
|
||||
//this is the real function, the other one is for debugging
|
||||
@ -736,28 +737,28 @@ function rarCopyString(length, distance) {
|
||||
destPtr = rOldBuffers[--l].data.length + destPtr;
|
||||
}
|
||||
//TODO: lets hope that it never needs to read beyond file boundaries
|
||||
while(length--) rBuffer.insertByte(rOldBuffers[l].data[destPtr++]);
|
||||
while (length--) rBuffer.insertByte(rOldBuffers[l].data[destPtr++]);
|
||||
}
|
||||
if (length > distance) {
|
||||
while(length--) rBuffer.insertByte(rBuffer.data[destPtr++]);
|
||||
while (length--) rBuffer.insertByte(rBuffer.data[destPtr++]);
|
||||
} else {
|
||||
rBuffer.insertBytes(rBuffer.data.subarray(destPtr, destPtr + length));
|
||||
}
|
||||
}
|
||||
|
||||
var rOldBuffers = []
|
||||
var rOldBuffers = [];
|
||||
// v must be a valid RarVolume
|
||||
function unpack(v) {
|
||||
|
||||
// TODO: implement what happens when unpVer is < 15
|
||||
// TODO: implement what happens when unpVer is < 15
|
||||
var Ver = v.header.unpVer <= 15 ? 15 : v.header.unpVer,
|
||||
Solid = v.header.LHD_SOLID,
|
||||
bstream = new bitjs.io.BitStream(v.fileData.buffer, true /* rtl */, v.fileData.byteOffset, v.fileData.byteLength );
|
||||
|
||||
|
||||
rBuffer = new bitjs.io.ByteBuffer(v.header.unpackedSize);
|
||||
|
||||
info("Unpacking " + v.filename+" RAR v" + Ver);
|
||||
|
||||
info("Unpacking " + v.filename + " RAR v" + Ver);
|
||||
|
||||
switch(Ver) {
|
||||
case 15: // rar 1.5 compression
|
||||
Unpack15(); //(bstream, Solid);
|
||||
@ -768,10 +769,10 @@ function unpack(v) {
|
||||
break;
|
||||
case 29: // rar 3.x compression
|
||||
case 36: // alternative hash
|
||||
Unpack29(bstream, Solid);
|
||||
Unpack29(bstream);
|
||||
break;
|
||||
} // switch(method)
|
||||
|
||||
|
||||
rOldBuffers.push(rBuffer);
|
||||
//TODO: clear these old buffers when there's over 4MB of history
|
||||
return rBuffer.data;
|
||||
@ -779,10 +780,10 @@ function unpack(v) {
|
||||
|
||||
// bstream is a bit stream
|
||||
var RarLocalFile = function(bstream) {
|
||||
|
||||
|
||||
this.header = new RarVolumeHeader(bstream);
|
||||
this.filename = this.header.filename;
|
||||
|
||||
|
||||
if (this.header.headType != FILE_HEAD && this.header.headType != ENDARC_HEAD) {
|
||||
this.isValid = false;
|
||||
info("Error! RAR Volume did not include a FILE_HEAD header ");
|
||||
@ -803,7 +804,7 @@ RarLocalFile.prototype.unrar = function() {
|
||||
if (this.header.method === 0x30) {
|
||||
info("Unstore " + this.filename);
|
||||
this.isValid = true;
|
||||
|
||||
|
||||
currentBytesUnarchivedInFile += this.fileData.length;
|
||||
currentBytesUnarchived += this.fileData.length;
|
||||
|
||||
@ -817,7 +818,7 @@ RarLocalFile.prototype.unrar = function() {
|
||||
this.fileData = unpack(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var unrar = function(arrayBuffer) {
|
||||
currentFilename = "";
|
||||
@ -834,16 +835,16 @@ var unrar = function(arrayBuffer) {
|
||||
if (header.crc === 0x6152 &&
|
||||
header.headType === 0x72 &&
|
||||
header.flags.value === 0x1A21 &&
|
||||
header.headSize === 7)
|
||||
{
|
||||
header.headSize === 7) {
|
||||
|
||||
info("Found RAR signature");
|
||||
|
||||
var mhead = new RarVolumeHeader(bstream);
|
||||
if (mhead.headType != MAIN_HEAD) {
|
||||
info("Error! RAR did not include a MAIN_HEAD header");
|
||||
} else {
|
||||
var localFiles = [],
|
||||
localFile = null;
|
||||
var localFiles = [];
|
||||
var localFile = null;
|
||||
do {
|
||||
try {
|
||||
localFile = new RarLocalFile(bstream);
|
||||
@ -852,24 +853,24 @@ var unrar = function(arrayBuffer) {
|
||||
totalUncompressedBytesInArchive += localFile.header.unpackedSize;
|
||||
localFiles.push(localFile);
|
||||
} else if (localFile.header.packSize === 0 && localFile.header.unpackedSize === 0) {
|
||||
localFile.isValid = true;
|
||||
localFile.isValid = true;
|
||||
}
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
break;
|
||||
}
|
||||
//info("bstream" + bstream.bytePtr+"/"+bstream.bytes.length);
|
||||
} while( localFile.isValid );
|
||||
} while ( localFile.isValid );
|
||||
totalFilesInArchive = localFiles.length;
|
||||
|
||||
// now we have all information but things are unpacked
|
||||
// TODO: unpack
|
||||
localFiles = localFiles.sort(function(a,b) {
|
||||
localFiles = localFiles.sort(function(a, b) {
|
||||
var aname = a.filename.toLowerCase();
|
||||
var bname = b.filename.toLowerCase();
|
||||
return aname > bname ? 1 : -1;
|
||||
});
|
||||
|
||||
info(localFiles.map(function(a) {return a.filename}).join(', '));
|
||||
info(localFiles.map(function(a) {return a.filename;}).join(", "));
|
||||
for (var i = 0; i < localFiles.length; ++i) {
|
||||
var localfile = localFiles[i];
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
* ZIP format: http://www.pkware.com/documents/casestudies/APPNOTE.TXT
|
||||
* DEFLATE format: http://tools.ietf.org/html/rfc1951
|
||||
*/
|
||||
/* global bitjs */
|
||||
/* global bitjs, importScripts, Uint8Array*/
|
||||
|
||||
// This file expects to be invoked as a Worker (see onmessage below).
|
||||
importScripts("io.js");
|
||||
@ -44,12 +44,10 @@ var zLocalFileHeaderSignature = 0x04034b50;
|
||||
var zArchiveExtraDataSignature = 0x08064b50;
|
||||
var zCentralFileHeaderSignature = 0x02014b50;
|
||||
var zDigitalSignatureSignature = 0x05054b50;
|
||||
var zEndOfCentralDirSignature = 0x06064b50;
|
||||
var zEndOfCentralDirLocatorSignature = 0x07064b50;
|
||||
|
||||
// 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(){}) {
|
||||
if (typeof bstream != typeof {} || !bstream.readNumber || typeof bstream.readNumber != typeof function() {}) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -112,9 +110,10 @@ 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)");
|
||||
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);
|
||||
}
|
||||
// version == 20, compression method == 8 (DEFLATE)
|
||||
else if (this.compressionMethod == 8) {
|
||||
@ -158,7 +157,7 @@ var unzip = function(arrayBuffer) {
|
||||
totalFilesInArchive = localFiles.length;
|
||||
|
||||
// got all local files, now sort them
|
||||
localFiles.sort(function(a,b) {
|
||||
localFiles.sort(function(a, b) {
|
||||
var aname = a.filename.toLowerCase();
|
||||
var bname = b.filename.toLowerCase();
|
||||
return aname > bname ? 1 : -1;
|
||||
@ -238,11 +237,11 @@ var unzip = function(arrayBuffer) {
|
||||
}
|
||||
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
|
||||
// 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
|
||||
@ -253,7 +252,7 @@ function getHuffmanCodes(bitLengths) {
|
||||
|
||||
// Reference: http://tools.ietf.org/html/rfc1951#page-8
|
||||
var numLengths = bitLengths.length,
|
||||
bl_count = [],
|
||||
blCount = [],
|
||||
MAX_BITS = 1;
|
||||
|
||||
// Step 1: count up how many codes of each length we have
|
||||
@ -265,22 +264,22 @@ function getHuffmanCodes(bitLengths) {
|
||||
return null;
|
||||
}
|
||||
// increment the appropriate bitlength count
|
||||
if (bl_count[length] == undefined) bl_count[length] = 0;
|
||||
if (blCount[length] == undefined) blCount[length] = 0;
|
||||
// a length of zero means this symbol is not participating in the huffman coding
|
||||
if (length > 0) bl_count[length]++;
|
||||
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 next_code = [],
|
||||
var nextCode = [],
|
||||
code = 0;
|
||||
for (var bits = 1; bits <= MAX_BITS; ++bits) {
|
||||
var length = bits-1;
|
||||
var length = bits - 1;
|
||||
// ensure undefined lengths are zero
|
||||
if (bl_count[length] == undefined) bl_count[length] = 0;
|
||||
code = (code + bl_count[bits-1]) << 1;
|
||||
next_code[bits] = code;
|
||||
if (blCount[length] == undefined) blCount[length] = 0;
|
||||
code = (code + blCount[bits - 1]) << 1;
|
||||
nextCode [bits] = code;
|
||||
}
|
||||
|
||||
// Step 3: Assign numerical values to all codes
|
||||
@ -288,9 +287,9 @@ function getHuffmanCodes(bitLengths) {
|
||||
for (var n = 0; n < numLengths; ++n) {
|
||||
var len = bitLengths[n];
|
||||
if (len != 0) {
|
||||
table[next_code[len]] = { length: len, symbol: n }; //, bitstring: binaryValueToString(next_code[len],len) };
|
||||
table[nextCode [len]] = { length: len, symbol: n }; //, bitstring: binaryValueToString(nextCode [len],len) };
|
||||
tableLength++;
|
||||
next_code[len]++;
|
||||
nextCode [len]++;
|
||||
}
|
||||
}
|
||||
table.maxLength = tableLength;
|
||||
@ -321,7 +320,8 @@ function getFixedLiteralTable() {
|
||||
// create once
|
||||
if (!fixedHCtoLiteral) {
|
||||
var bitlengths = new Array(288);
|
||||
for (var i = 0; i <= 143; ++i) bitlengths[i] = 8;
|
||||
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;
|
||||
@ -335,7 +335,9 @@ function getFixedDistanceTable() {
|
||||
// create once
|
||||
if (!fixedHCtoDistance) {
|
||||
var bitlengths = new Array(32);
|
||||
for (var i = 0; i < 32; ++i) { bitlengths[i] = 5; }
|
||||
for (var i = 0; i < 32; ++i) {
|
||||
bitlengths[i] = 5;
|
||||
}
|
||||
|
||||
// get huffman code table
|
||||
fixedHCtoDistance = getHuffmanCodes(bitlengths);
|
||||
@ -347,13 +349,12 @@ function getFixedDistanceTable() {
|
||||
// then return that symbol
|
||||
function decodeSymbol(bstream, hcTable) {
|
||||
var code = 0, len = 0;
|
||||
var match = false;
|
||||
|
||||
// loop until we match
|
||||
for (;;) {
|
||||
// read in next bit
|
||||
var bit = bstream.readBits(1);
|
||||
code = (code<<1) | bit;
|
||||
code = (code << 1) | bit;
|
||||
++len;
|
||||
|
||||
// check against Huffman Code table and break if found
|
||||
@ -372,62 +373,62 @@ function decodeSymbol(bstream, hcTable) {
|
||||
|
||||
|
||||
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
|
||||
/*
|
||||
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]
|
||||
[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
|
||||
*/
|
||||
/*
|
||||
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]
|
||||
[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) {
|
||||
@ -446,10 +447,9 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) {
|
||||
stream, and copy length bytes from this
|
||||
position to the output stream.
|
||||
*/
|
||||
var numSymbols = 0, blockSize = 0;
|
||||
var blockSize = 0;
|
||||
for (;;) {
|
||||
var symbol = decodeSymbol(bstream, hcLiteralTable);
|
||||
++numSymbols;
|
||||
if (symbol < 256) {
|
||||
// copy literal byte to output
|
||||
buffer.insertByte(symbol);
|
||||
@ -461,7 +461,7 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) {
|
||||
break;
|
||||
}
|
||||
else {
|
||||
var lengthLookup = LengthLookupTable[symbol-257],
|
||||
var lengthLookup = LengthLookupTable[symbol - 257],
|
||||
length = lengthLookup[1] + bstream.readBits(lengthLookup[0]),
|
||||
distLookup = DistLookupTable[decodeSymbol(bstream, hcDistanceTable)],
|
||||
distance = distLookup[1] + bstream.readBits(distLookup[0]);
|
||||
@ -479,13 +479,13 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) {
|
||||
// loop for each character
|
||||
var ch = buffer.ptr - distance;
|
||||
blockSize += length;
|
||||
if(length > distance) {
|
||||
var data = buffer.data;
|
||||
while (length--) {
|
||||
buffer.insertByte(data[ch++]);
|
||||
}
|
||||
if (length > distance) {
|
||||
var data = buffer.data;
|
||||
while (length--) {
|
||||
buffer.insertByte(data[ch++]);
|
||||
}
|
||||
} else {
|
||||
buffer.insertBytes(buffer.data.subarray(ch, ch + length))
|
||||
buffer.insertBytes(buffer.data.subarray(ch, ch + length));
|
||||
}
|
||||
|
||||
} // length-distance pair
|
||||
@ -494,6 +494,16 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) {
|
||||
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
|
||||
@ -516,11 +526,11 @@ function inflate(compressedData, numDecompressedBytes) {
|
||||
if (bType == 0) {
|
||||
// skip remaining bits in this byte
|
||||
while (bstream.bitPtr != 0) bstream.readBits(1);
|
||||
var len = bstream.readBits(16),
|
||||
nlen = bstream.readBits(16);
|
||||
// TODO: check if nlen is the ones-complement of len?
|
||||
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));
|
||||
if (len > 0) buffer.insertBytes(bstream.readBytes(len));
|
||||
blockSize = len;
|
||||
}
|
||||
// fixed Huffman codes
|
||||
@ -573,14 +583,13 @@ function inflate(compressedData, numDecompressedBytes) {
|
||||
}
|
||||
}
|
||||
else if (symbol == 17) {
|
||||
var repeat = bstream.readBits(3) + 3;
|
||||
while (repeat--) {
|
||||
var repeat1 = bstream.readBits(3) + 3;
|
||||
while (repeat1--) {
|
||||
literalCodeLengths.push(0);
|
||||
}
|
||||
}
|
||||
else if (symbol == 18) {
|
||||
var repeat = bstream.readBits(7) + 11;
|
||||
while (repeat--) {
|
||||
} else if (symbol == 18) {
|
||||
var repeat2 = bstream.readBits(7) + 11;
|
||||
while (repeat2--) {
|
||||
literalCodeLengths.push(0);
|
||||
}
|
||||
}
|
||||
@ -593,9 +602,8 @@ function inflate(compressedData, numDecompressedBytes) {
|
||||
var hcLiteralTable = getHuffmanCodes(literalCodeLengths),
|
||||
hcDistanceTable = getHuffmanCodes(distanceCodeLengths);
|
||||
blockSize = inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer);
|
||||
}
|
||||
// error
|
||||
else {
|
||||
} else {
|
||||
// error
|
||||
err("Error! Encountered deflate block of type 3");
|
||||
return null;
|
||||
}
|
||||
|
199
cps/static/js/uploadprogress.js
Normal file
199
cps/static/js/uploadprogress.js
Normal file
@ -0,0 +1,199 @@
|
||||
/*
|
||||
* bootstrap-uploadprogress
|
||||
* github: https://github.com/jakobadam/bootstrap-uploadprogress
|
||||
*
|
||||
* Copyright (c) 2015 Jakob Aarøe Dam
|
||||
* Version 1.0.0
|
||||
* Licensed under the MIT license.
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
"use strict";
|
||||
|
||||
$.support.xhrFileUpload = !!(window.FileReader && window.ProgressEvent);
|
||||
$.support.xhrFormData = !!window.FormData;
|
||||
|
||||
if (!$.support.xhrFileUpload || !$.support.xhrFormData) {
|
||||
// skip decorating form
|
||||
return;
|
||||
}
|
||||
|
||||
var template = "<div class=\"modal fade\" id=\"file-progress-modal\">" +
|
||||
"<div class=\"modal-dialog upload-modal-dialog\">" +
|
||||
" <div class=\"modal-content\">" +
|
||||
" <div class=\"modal-header\">" +
|
||||
" <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">×</span></button>" +
|
||||
" <h4 class=\"modal-title\">Uploading</h4>" +
|
||||
" </div>" +
|
||||
" <div class=\"modal-body\">" +
|
||||
" <div class=\"modal-message\"></div>" +
|
||||
" <div class=\"progress\">" +
|
||||
" <div class=\"progress-bar progress-bar-striped active\" role=\"progressbar\" aria-valuenow=\"0\" aria-valuemin=\"0\"" +
|
||||
" aria-valuemax=\"100\" style=\"width: 0%;min-width: 2em;\">" +
|
||||
" 0%" +
|
||||
" </div>" +
|
||||
" </div>" +
|
||||
" </div>" +
|
||||
" <div class=\"modal-footer\" style=\"display:none\">" +
|
||||
" <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>" +
|
||||
" </div>" +
|
||||
" </div>" +
|
||||
" </div>" +
|
||||
"</div>";
|
||||
|
||||
var UploadProgress = function(element, options) {
|
||||
this.options = options;
|
||||
this.$element = $(element);
|
||||
};
|
||||
|
||||
UploadProgress.prototype = {
|
||||
|
||||
constructor: function() {
|
||||
this.$form = this.$element;
|
||||
this.$form.on("submit", $.proxy(this.submit, this));
|
||||
this.$modal = $(this.options.template);
|
||||
this.$modalTitle = this.$modal.find(".modal-title");
|
||||
this.$modalFooter = this.$modal.find(".modal-footer");
|
||||
this.$modalBar = this.$modal.find(".progress-bar");
|
||||
|
||||
// Translate texts
|
||||
this.$modalTitle.text(this.options.modalTitle)
|
||||
this.$modalFooter.children("button").text(this.options.modalFooter);
|
||||
|
||||
this.$modal.on("hidden.bs.modal", $.proxy(this.reset, this));
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.$modalTitle.text(this.options.modalTitle);
|
||||
this.$modalFooter.hide();
|
||||
this.$modalBar.addClass("progress-bar-success");
|
||||
this.$modalBar.removeClass("progress-bar-danger");
|
||||
if (this.xhr) {
|
||||
this.xhr.abort();
|
||||
}
|
||||
},
|
||||
|
||||
submit: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.$modal.modal({
|
||||
backdrop: "static",
|
||||
keyboard: false
|
||||
});
|
||||
|
||||
// We need the native XMLHttpRequest for the progress event
|
||||
var xhr = new XMLHttpRequest();
|
||||
this.xhr = xhr;
|
||||
|
||||
xhr.addEventListener("load", $.proxy(this.success, this, xhr));
|
||||
xhr.addEventListener("error", $.proxy(this.error, this, xhr));
|
||||
|
||||
xhr.upload.addEventListener("progress", $.proxy(this.progress, this));
|
||||
|
||||
var form = this.$form;
|
||||
|
||||
xhr.open(form.attr("method"), form.attr("action"));
|
||||
xhr.setRequestHeader("X-REQUESTED-WITH", "XMLHttpRequest");
|
||||
|
||||
var data = new FormData(form.get(0));
|
||||
xhr.send(data);
|
||||
},
|
||||
|
||||
success: function(xhr) {
|
||||
if (xhr.status === 0 || xhr.status >= 400) {
|
||||
// HTTP 500 ends up here!?!
|
||||
return this.error(xhr);
|
||||
}
|
||||
this.setProgress(100);
|
||||
var url;
|
||||
var contentType = xhr.getResponseHeader("Content-Type");
|
||||
|
||||
// make it possible to return the redirect URL in
|
||||
// a JSON response
|
||||
if (contentType.indexOf("application/json") !== -1) {
|
||||
var response = $.parseJSON(xhr.responseText);
|
||||
url = response.location;
|
||||
}
|
||||
else{
|
||||
url = this.options.redirect_url;
|
||||
}
|
||||
window.location.href = url;
|
||||
},
|
||||
|
||||
// handle form error
|
||||
// we replace the form with the returned one
|
||||
error: function(xhr) {
|
||||
this.$modalTitle.text(this.options.modalTitleFailed);
|
||||
|
||||
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) {
|
||||
var responseText = xhr.responseText;
|
||||
if (contentType.indexOf("text/plain") !== -1) {
|
||||
responseText = "<pre>" + responseText + "</pre>";
|
||||
document.write(responseText);
|
||||
}
|
||||
else {
|
||||
this.$modalBar.text(responseText);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.$modalBar.text(this.options.modalTitleFailed);
|
||||
}
|
||||
},
|
||||
|
||||
setProgress: function(percent) {
|
||||
var txt = percent + "%";
|
||||
if (percent === 100) {
|
||||
txt = this.options.uploadedMsg;
|
||||
}
|
||||
this.$modalBar.attr("aria-valuenow", percent);
|
||||
this.$modalBar.text(txt);
|
||||
this.$modalBar.css("width", percent + "%");
|
||||
},
|
||||
|
||||
progress: function(/*ProgressEvent*/e) {
|
||||
var percent = Math.round((e.loaded / e.total) * 100);
|
||||
this.setProgress(percent);
|
||||
},
|
||||
|
||||
// replaceForm replaces the contents of the current form
|
||||
// with the form in the html argument.
|
||||
// We use the id of the current form to find the new form in the html
|
||||
replaceForm: function(html) {
|
||||
var newForm;
|
||||
var formId = this.$form.attr("id");
|
||||
if ( typeof formId !== "undefined") {
|
||||
newForm = $(html).find("#" + formId);
|
||||
} else {
|
||||
newForm = $(html).find("form");
|
||||
}
|
||||
// add the filestyle again
|
||||
newForm.find(":file").filestyle({buttonBefore: true});
|
||||
this.$form.html(newForm.children());
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.uploadprogress = function(options) {
|
||||
return this.each(function() {
|
||||
var _options = $.extend({}, $.fn.uploadprogress.defaults, options);
|
||||
var fileProgress = new UploadProgress(this, _options);
|
||||
fileProgress.constructor();
|
||||
});
|
||||
};
|
||||
|
||||
$.fn.uploadprogress.defaults = {
|
||||
template: template,
|
||||
uploadedMsg: "Upload done, processing, please wait...",
|
||||
modalTitle: "Uploading",
|
||||
modalFooter: "Close",
|
||||
modalTitleFailed: "Upload failed"
|
||||
//redirect_url: ...
|
||||
// need to customize stuff? Add here, and change code accordingly.
|
||||
};
|
||||
|
||||
})(window.jQuery);
|
@ -105,7 +105,7 @@
|
||||
<div class="col">
|
||||
<h2>{{_('Administration')}}</h2>
|
||||
<div class="btn btn-default" id="restart_database">{{_('Reconnect to Calibre DB')}}</div>
|
||||
<div class="btn btn-default" id="admin_restart"data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-Web')}}</div>
|
||||
<div class="btn btn-default" id="admin_restart" data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-Web')}}</div>
|
||||
<div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-Web')}}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -144,7 +144,7 @@
|
||||
<div class="modal-body text-center">
|
||||
<p>{{_('Do you really want to restart Calibre-Web?')}}</p>
|
||||
<div id="spinner" class="spinner" style="display:none;">
|
||||
<img id="img-spinner" src="/static/css/images/loading-icon.gif"/>
|
||||
<img id="img-spinner" src="{{ url_for('static', filename='css/images/loading-icon.gif') }}"/>
|
||||
</div>
|
||||
<p></p>
|
||||
<button type="button" class="btn btn-default" id="restart" >{{_('Ok')}}</button>
|
||||
@ -176,7 +176,7 @@
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<div id="spinner2" class="spinner2" style="display:none;">
|
||||
<img id="img-spinner" src="/static/css/images/loading-icon.gif"/>
|
||||
<img id="img-spinner2" src="{{ url_for('static', filename='css/images/loading-icon.gif') }}"/>
|
||||
</div>
|
||||
<p></p>
|
||||
<div id="Updatecontent"></div>
|
||||
|
@ -29,20 +29,32 @@
|
||||
<div class="cover">
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}">
|
||||
{% if entry.has_cover %}
|
||||
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" />
|
||||
<img src="{{ url_for('get_cover', book_id=entry.id) }}" />
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" />
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
</a>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a>
|
||||
{% if not loop.last %}
|
||||
&
|
||||
{% endif %}
|
||||
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
|
||||
{% if not loop.first %}
|
||||
<span class="author-hidden-divider">&</span>
|
||||
{% endif %}
|
||||
<a class="author-name author-hidden" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% if loop.last %}
|
||||
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if not loop.first %}
|
||||
<span>&</span>
|
||||
{% endif %}
|
||||
<a class="author-name" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% if entry.ratings.__len__() > 0 %}
|
||||
@ -78,13 +90,15 @@
|
||||
<div class="meta">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="https://www.goodreads.com/author/show/{{ author.gid }}" target="_blank" rel="noopener">
|
||||
{{author.name.replace('|',',')}}
|
||||
</a>
|
||||
{% if not loop.last %}
|
||||
&
|
||||
{% endif %}
|
||||
{% for author in entry.authors %}
|
||||
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
|
||||
<a class="author-name author-hidden" href="https://www.goodreads.com/author/show/{{ author.gid }}" target="_blank" rel="noopener">{{author.name.replace('|',',')}}</a>
|
||||
{% if loop.last %}
|
||||
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a class="author-name" href="https://www.goodreads.com/author/show/{{ author.gid }}" target="_blank" rel="noopener">{{author.name.replace('|',',')}}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
<div class="rating">
|
||||
|
@ -6,7 +6,7 @@
|
||||
<div class="col-sm-3 col-lg-3 col-xs-12">
|
||||
<div class="cover">
|
||||
{% if book.has_cover %}
|
||||
<img src="{{ url_for('get_cover', cover_path=book.path.replace('\\','/')) }}" alt="{{ book.title }}"/>
|
||||
<img src="{{ url_for('get_cover', book_id=book.id) }}" alt="{{ book.title }}"/>
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ book.title }}"/>
|
||||
{% endif %}
|
||||
@ -101,7 +101,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="publisher">{{_('Publisher')}}</label>
|
||||
<input type="text" class="form-control typeahead" name="publisher" id="publisher" value="{% if book.publishers|length > 0 %}{{book.publishers[0].name}}{% endif %}" disabled>
|
||||
<input type="text" class="form-control typeahead" name="publisher" id="publisher" value="{% if book.publishers|length > 0 %}{{book.publishers[0].name}}{% endif %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="languages">{{_('Language')}}</label>
|
||||
@ -219,7 +219,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<div>{{_('Click the cover to load metadata to the form')}}</div>
|
||||
<div class="text-center"><strong>{{_('Click the cover to load metadata to the form')}}</strong></div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="text-center padded-bottom">
|
||||
|
@ -31,15 +31,19 @@
|
||||
</label>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if show_authenticate_google_drive and g.user.is_authenticated %}
|
||||
{% if show_authenticate_google_drive and g.user.is_authenticated and content.config_use_google_drive %}
|
||||
<div class="form-group required">
|
||||
<a href="{{ url_for('authenticate_google_drive') }}" class="btn btn-primary">{{_('Authenticate Google Drive')}}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if show_authenticate_google_drive and not g.user.is_authenticated %}
|
||||
{% if show_authenticate_google_drive and g.user.is_authenticated and not content.config_use_google_drive %}
|
||||
<div >{{_('Please hit submit to continue with setup')}}</div>
|
||||
{% endif %}
|
||||
{% if not g.user.is_authenticated %}
|
||||
<div >{{_('Please finish Google Drive setup after login')}}</div>
|
||||
{% endif %}
|
||||
{% if not show_authenticate_google_drive %}
|
||||
{% 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">
|
||||
@ -58,6 +62,7 @@
|
||||
<a href="{{ url_for('watch_gdrive') }}" class="btn btn-primary">Enable watch of metadata.db</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -76,7 +81,7 @@
|
||||
</div>
|
||||
<div id="collapsetwo" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label for="config_port">{{_('Server Port')}}</label>
|
||||
<input type="number" min="1" max="65535" class="form-control" name="config_port" id="config_port" value="{% if content.config_port != None %}{{ content.config_port }}{% endif %}" autocomplete="off" required>
|
||||
</div>
|
||||
@ -88,6 +93,15 @@
|
||||
<label for="config_keyfile">{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
|
||||
<input type="text" class="form-control" name="config_keyfile" id="config_keyfile" value="{% if content.config_keyfile != None %}{{ content.config_keyfile }}{% endif %}" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="config_updater">{{_('Update channel')}}</label>
|
||||
<select name="config_updater" id="config_updater" class="form-control">
|
||||
<option value="0" {% if content.config_updatechannel == 0 %}selected{% endif %}>{{_('Stable')}}</option>
|
||||
<!--option value="1" {% if content.config_updatechannel == 1 %}selected{% endif %}>{{_('Stable (Automatic)')}}</option-->
|
||||
<option value="2" {% if content.config_updatechannel == 2 %}selected{% endif %}>{{_('Nightly')}}</option>
|
||||
<!--option-- value="3" {% if content.config_updatechannel == 3 %}selected{% endif %}>{{_('Nightly (Automatic)')}}</option-->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -27,6 +27,17 @@
|
||||
<label for="config_random_books">{{_('No. of random books to show')}}</label>
|
||||
<input type="number" min="1" max="30" class="form-control" name="config_random_books" id="config_random_books" value="{% if content.config_random_books != None %}{{ content.config_random_books }}{% endif %}" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="config_authors_max">{{_('No. of authors to show before hiding (0=disable hiding)')}}</label>
|
||||
<input type="number" min="0" max="999" class="form-control" name="config_authors_max" id="config_authors_max" value="{% if content.config_authors_max != None %}{{ content.config_authors_max }}{% endif %}" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="config_theme">{{_('Theme')}}</label>
|
||||
<select name="config_theme" id="config_theme" class="form-control">
|
||||
<option value="0" {% if content.config_theme == 0 %}selected{% endif %}>{{ _("Standard Theme") }}</option>
|
||||
<option value="1" {% if content.config_theme == 1 %}selected{% endif %}>{{ _("caliBlur! Dark Theme") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="config_columns_to_ignore">{{_('Regular expression for ignoring columns')}}</label>
|
||||
<input type="text" class="form-control" name="config_columns_to_ignore" id="config_columns_to_ignore" value="{% if content.config_columns_to_ignore != None %}{{ content.config_columns_to_ignore }}{% endif %}" autocomplete="off">
|
||||
@ -143,6 +154,10 @@
|
||||
<input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}>
|
||||
<label for="show_author">{{_('Show author selection')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="show_publisher" id="show_publisher" {% if content.show_publisher() %}checked{% endif %}>
|
||||
<label for="show_publisher">{{_('Show publisher selection')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}>
|
||||
<label for="show_read_and_unread">{{_('Show read and unread')}}</label>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="col-sm-3 col-lg-3 col-xs-5">
|
||||
<div class="cover">
|
||||
{% if entry.has_cover %}
|
||||
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" />
|
||||
<img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
|
||||
{% endif %}
|
||||
@ -18,7 +18,7 @@
|
||||
{% if entry.data|length %}
|
||||
<div class="btn-group" role="group">
|
||||
{% if entry.data|length < 2 %}
|
||||
<button type="button" class="btn btn-primary">
|
||||
<button id="Download" type="button" class="btn btn-primary">
|
||||
{{_('Download')}} :
|
||||
</button>
|
||||
{% for format in entry.data %}
|
||||
@ -40,20 +40,32 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if g.user.kindle_mail and g.user.is_authenticated %}
|
||||
<a href="{{url_for('send_to_kindle', book_id=entry.id)}}" id="sendbtn" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-send"></span> {{_('Send to Kindle')}}</a>
|
||||
{% if g.user.kindle_mail and g.user.is_authenticated and kindle_list %}
|
||||
{% if kindle_list.__len__() == 1 %}
|
||||
<a href="{{url_for('send_to_kindle', book_id=entry.id, book_format=kindle_list[0]['format'], convert=kindle_list[0]['convert'])}}" id="sendbtn" data-text="{{_('Send to Kindle')}}" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-send"></span> {{kindle_list[0]['text']}}</a>
|
||||
{% else %}
|
||||
<div class="btn-group" role="group">
|
||||
<button id="sendbtn2" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-send"></span>{{_('Send to Kindle')}}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="send-to-kindle">
|
||||
{% for format in kindle_list %}
|
||||
<li><a href="{{url_for('send_to_kindle', book_id=entry.id, book_format=format['format'], convert=format['convert'])}}">{{format['text']}}</a></li>
|
||||
{%endfor%}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if entry.data|length %}
|
||||
{% if reader_list %}
|
||||
<div class="btn-group" role="group">
|
||||
<button id="read-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-eye-open"></span> {{_('Read in browser')}}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="read-in-browser">
|
||||
{% for format in entry.data %}
|
||||
{%if format.format|lower == 'epub' or format.format|lower == 'txt' or format.format|lower == 'pdf' or format.format|lower == 'cbr' or format.format|lower == 'cbt' or format.format|lower == 'cbz' %}
|
||||
<li><a target="_blank" href="{{ url_for('read_book', book_id=entry.id, book_format=format.format|lower) }}">{{format.format}}</a></li>
|
||||
{% endif %}
|
||||
{% for format in reader_list %}
|
||||
<li><a target="_blank" href="{{ url_for('read_book', book_id=entry.id, book_format=format) }}">{{format}}</a></li>
|
||||
{%endfor%}
|
||||
</ul>
|
||||
</div>
|
||||
@ -91,7 +103,7 @@
|
||||
{% if entry.languages.__len__() > 0 %}
|
||||
<div class="languages">
|
||||
<p>
|
||||
<span class="label label-default">{{_('language')}}: {% for language in entry.languages %} {{language.language_name}}{% if not loop.last %},{% endif %}{% endfor %} </span>
|
||||
<span class="label label-default">{{_('language')}}: {% for language in entry.languages %}{{language.language_name}}{% if not loop.last %}, {% endif %}{% endfor %}</span>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -120,15 +132,21 @@
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.publishers|length > 0 %}
|
||||
<div class="publishers">
|
||||
<p>
|
||||
<span>{{_('Publisher')}}:{% for publisher in entry.publishers %} {{publisher.name}}{% if not loop.last %},{% endif %}{% endfor %}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>{{_('Publisher')}}:
|
||||
<a href="{{url_for('publisher', book_id=entry.publishers[0].id ) }}">{{entry.publishers[0].name}}</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.pubdate[:10] != '0101-01-01' %}
|
||||
<div class="publishing-date">
|
||||
<p>{{_('Publishing date')}}: {{entry.pubdate|formatdate}} </p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if cc|length > 0 %}
|
||||
|
||||
@ -164,7 +182,7 @@
|
||||
<p>
|
||||
<form id="have_read_form" action="{{ url_for('toggle_read', book_id=entry.id)}}" method="POST">
|
||||
<label class="block-label">
|
||||
<input id="have_read_cb" type="checkbox" {% if have_read %}checked{% endif %} >
|
||||
<input id="have_read_cb" data-checked="{{_('Mark As Unread')}}" data-unchecked="{{_('Mark As Read')}}" type="checkbox" {% if have_read %}checked{% endif %} >
|
||||
<span>{{_('Read')}}</span>
|
||||
</label>
|
||||
</form>
|
||||
|
@ -9,17 +9,29 @@
|
||||
<div class="cover">
|
||||
{% if entry.has_cover is defined %}
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" />
|
||||
<img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="meta">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
</a>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% if not loop.last %}
|
||||
&
|
||||
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
|
||||
{% if not loop.first %}
|
||||
<span class="author-hidden-divider">&</span>
|
||||
{% endif %}
|
||||
<a class="author-name author-hidden" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% if loop.last %}
|
||||
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if not loop.first %}
|
||||
<span>&</span>
|
||||
{% endif %}
|
||||
<a class="author-name" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
|
@ -27,7 +27,10 @@
|
||||
href="{{request.script_root + request.path}}?offset={{ pagination.previous_offset }}"
|
||||
type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
|
||||
{% endif %}
|
||||
<link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/>
|
||||
<link rel="search"
|
||||
href="{{url_for('feed_osd')}}"
|
||||
type="application/opensearchdescription+xml"/>
|
||||
<!--link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/-->
|
||||
<title>{{instance}}</title>
|
||||
<author>
|
||||
<name>{{instance}}</name>
|
||||
@ -40,9 +43,16 @@
|
||||
<title>{{entry.title}}</title>
|
||||
<id>{{entry.uuid}}</id>
|
||||
<updated>{{entry.atom_timestamp}}</updated>
|
||||
<author>
|
||||
<name>{{entry.authors[0].name}}</name>
|
||||
</author>
|
||||
{% if entry.authors.__len__() > 0 %}
|
||||
<author>
|
||||
<name>{{entry.authors[0].name}}</name>
|
||||
</author>
|
||||
{% endif %}
|
||||
{% if entry.publishers.__len__() > 0 %}
|
||||
<publisher>
|
||||
<name>{{entry.publishers[0].name}}</name>
|
||||
</publisher>
|
||||
{% endif %}
|
||||
<dcterms:language>{{entry.language}}</dcterms:language>
|
||||
{% for tag in entry.tags %}
|
||||
<category scheme="http://www.bisg.org/standards/bisac_subject/index.html"
|
||||
|
26
cps/templates/http_error.html
Normal file
26
cps/templates/http_error.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="http-error" lang="{{ g.user.locale }}">
|
||||
<head>
|
||||
<title>{{ instance }} | HTTP Error ({{ error_code }})</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link rel="apple-touch-icon" sizes="140x140" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen">
|
||||
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" media="screen">
|
||||
{% if g.current_theme == 1 %}
|
||||
<link href="{{ url_for('static', filename='css/caliBlur.min.css') }}" rel="stylesheet" media="screen">
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container text-center">
|
||||
<h1>{{ error_code }}</h1>
|
||||
<h3>{{ error_name }}</h3>
|
||||
<a href="{{url_for('index')}}" title="{{ _('Back to home') }}">{{_('Back to home')}}</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,8 +1,8 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
{% if g.user.show_detail_random() %}
|
||||
<div class="discover">
|
||||
<h2>{{_('Discover (Random Books)')}}</h2>
|
||||
<div class="discover random-books">
|
||||
<h2 class="random-books">{{_('Discover (Random Books)')}}</h2>
|
||||
<div class="row">
|
||||
|
||||
{% for entry in random %}
|
||||
@ -10,19 +10,31 @@
|
||||
<div class="cover">
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
{% if entry.has_cover %}
|
||||
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" />
|
||||
<img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
</a>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% if not loop.last %}
|
||||
&
|
||||
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
|
||||
{% if not loop.first %}
|
||||
<span class="author-hidden-divider">&</span>
|
||||
{% endif %}
|
||||
<a class="author-name author-hidden" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% if loop.last %}
|
||||
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if not loop.first %}
|
||||
<span>&</span>
|
||||
{% endif %}
|
||||
<a class="author-name" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
@ -45,7 +57,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="discover load-more">
|
||||
<h2>{{title}}</h2>
|
||||
<h2 class="{{title}}">{{_(title)}}</h2>
|
||||
<div class="row">
|
||||
{% if entries[0] %}
|
||||
{% for entry in entries %}
|
||||
@ -53,19 +65,31 @@
|
||||
<div class="cover">
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
{% if entry.has_cover %}
|
||||
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}"/>
|
||||
<img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}"/>
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
</a>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% if not loop.last %}
|
||||
&
|
||||
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
|
||||
{% if not loop.first %}
|
||||
<span class="author-hidden-divider">&</span>
|
||||
{% endif %}
|
||||
<a class="author-name author-hidden" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% if loop.last %}
|
||||
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if not loop.first %}
|
||||
<span>&</span>
|
||||
{% endif %}
|
||||
<a class="author-name" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
|
@ -5,7 +5,10 @@
|
||||
<link rel="self" href="{{url_for('feed_index')}}" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<link rel="start" title="{{_('Start')}}" href="{{url_for('feed_index')}}"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<link title="{{_('Search')}}" type="application/opensearchdescription+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/>
|
||||
<link rel="search"
|
||||
href="{{url_for('feed_osd')}}"
|
||||
type="application/opensearchdescription+xml"/>
|
||||
<!--link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/-->
|
||||
<title>{{instance}}</title>
|
||||
<author>
|
||||
<name>{{instance}}</name>
|
||||
@ -39,7 +42,7 @@
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Show Random Books')}}</content>
|
||||
</entry>
|
||||
{% if not current_user.is_anonymous %}
|
||||
{% if not current_user.is_anonymous %}
|
||||
<entry>
|
||||
<title>{{_('Read Books')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_read_books')}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
@ -47,7 +50,7 @@
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Read Books')}}</content>
|
||||
</entry>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<entry>
|
||||
<title>{{_('Unread Books')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_unread_books')}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
@ -61,35 +64,42 @@
|
||||
<id>{{url_for('feed_authorindex')}}</id>
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Books ordered by Author')}}</content>
|
||||
</entry>
|
||||
<entry>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>{{_('Publishers')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_publisherindex')}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
<id>{{url_for('feed_publisherindex')}}</id>
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Books ordered by publisher')}}</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>{{_('Category list')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_categoryindex')}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
<id>{{url_for('feed_categoryindex')}}</id>
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Books ordered by category')}}</content>
|
||||
</entry>
|
||||
<entry>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>{{_('Series list')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_seriesindex')}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
<id>{{url_for('feed_seriesindex')}}</id>
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Books ordered by series')}}</content>
|
||||
</entry>
|
||||
<entry>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>{{_('Public Shelves')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_shelfindex', public="public")}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
<id>{{url_for('feed_shelfindex', public="public")}}</id>
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Books organized in public shelfs, visible to everyone')}}</content>
|
||||
</entry>
|
||||
{% if not current_user.is_anonymous %}
|
||||
<entry>
|
||||
</entry>
|
||||
{% if not current_user.is_anonymous %}
|
||||
<entry>
|
||||
<title>{{_('Your Shelves')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_shelfindex')}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
<id>{{url_for('feed_shelfindex')}}</id>
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_("User's own shelfs, only visible to the current user himself")}}</content>
|
||||
</entry>
|
||||
{% endif %}
|
||||
</entry>
|
||||
{% endif %}
|
||||
</feed>
|
||||
|
@ -12,8 +12,9 @@
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen">
|
||||
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" media="screen">
|
||||
{% if g.user.get_theme == 1 %}
|
||||
<link href="{{ url_for('static', filename='css/caliBlur-style.css') }}" rel="stylesheet" media="screen">
|
||||
<link href="{{ url_for('static', filename='css/upload.css') }}" rel="stylesheet" media="screen">
|
||||
{% if g.current_theme == 1 %}
|
||||
<link href="{{ url_for('static', filename='css/caliBlur.min.css') }}" rel="stylesheet" media="screen">
|
||||
{% endif %}
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
@ -24,7 +25,7 @@
|
||||
|
||||
{% block header %}{% endblock %}
|
||||
</head>
|
||||
<body class="{{ page }}">
|
||||
<body class="{{ page }}" data-text="{{_('Home')}}" data-textback="{{_('Back')}}">
|
||||
<!-- Static navbar -->
|
||||
<div class="navbar navbar-default navbar-static-top" role="navigation">
|
||||
<div class="container-fluid">
|
||||
@ -61,21 +62,22 @@
|
||||
<li>
|
||||
<form id="form-upload" class="navbar-form" action="{{ url_for('upload') }}" method="post" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload" type="file" multiple></span>
|
||||
<span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload"
|
||||
type="file" accept="{% for format in accept %}.{{format}}{{ ',' if not loop.last }}{% endfor %}" multiple></span>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if g.user.role_admin() %}
|
||||
<li><a id="top_tasks" href="{{url_for('get_tasks_status')}}"><span class="glyphicon glyphicon-tasks"></span><span class="hidden-sm"> {{_('Tasks')}}</span></a></li>
|
||||
{% endif %}
|
||||
{% if g.user.role_admin() %}
|
||||
<li><a id="top_admin" href="{{url_for('admin')}}"><span class="glyphicon glyphicon-dashboard"></span><span class="hidden-sm"> {{_('Admin')}}</span></a></li>
|
||||
{% endif %}
|
||||
<li><a id="top_user" href="{{url_for('profile')}}"><span class="glyphicon glyphicon-user"></span><span class="hidden-sm"> {{g.user.nickname}}</span></a></li>
|
||||
{% if not g.user.is_anonymous %}
|
||||
<li><a id="logout" href="{{url_for('logout')}}"><span class="glyphicon glyphicon-log-out"></span><span class="hidden-sm"> {{_('Logout')}}</span></a></li>
|
||||
<li><a id="top_tasks" href="{{url_for('get_tasks_status')}}"><span class="glyphicon glyphicon-tasks"></span><span class="hidden-sm">{{_('Tasks')}}</span></a></li>
|
||||
{% endif %}
|
||||
{% if g.user.role_admin() %}
|
||||
<li><a id="top_admin" data-text="{{_('Settings')}}" href="{{url_for('admin')}}"><span class="glyphicon glyphicon-dashboard"></span><span class="hidden-sm">{{_('Admin')}}</span></a></li>
|
||||
{% endif %}
|
||||
<li><a id="top_user" data-text="{{_('Account')}}" href="{{url_for('profile')}}"><span class="glyphicon glyphicon-user"></span><span class="hidden-sm">{{g.user.nickname}}</span></a></li>
|
||||
{% if not g.user.is_anonymous %}
|
||||
<li><a id="logout" href="{{url_for('logout')}}"><span class="glyphicon glyphicon-log-out"></span><span class="hidden-sm">{{_('Logout')}}</span></a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if g.allow_registration and not g.user.is_authenticated %}
|
||||
@ -103,14 +105,14 @@
|
||||
</div>
|
||||
{%endif%}
|
||||
{% endfor %}
|
||||
<div id="loader" hidden="true">
|
||||
<center>
|
||||
<h3>{{_('Uploading...')}}</h3>
|
||||
<span>{{_("please don't refresh the page")}}</span>.
|
||||
<br />
|
||||
<img src="{{ url_for('static', filename='img/loader.gif') }}">
|
||||
</center>
|
||||
</div>
|
||||
{% if g.current_theme == 1 %}
|
||||
<div id="loader" hidden="true">
|
||||
<center>
|
||||
<h3>{{_('Uploading...')}}</h3>
|
||||
<span>{{_("please don't refresh the page")}}</span>.
|
||||
</center>
|
||||
</div>
|
||||
{%endif%}
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
{% if g.user.is_authenticated or g.user.is_anonymous %}
|
||||
@ -159,15 +161,18 @@
|
||||
{% if g.user.show_author() %}
|
||||
<li id="nav_author" {% if page == 'author' %}class="active"{% endif %}><a href="{{url_for('author_list')}}"><span class="glyphicon glyphicon-user"></span>{{_('Authors')}}</a></li>
|
||||
{%endif%}
|
||||
{% if g.user.show_publisher() %}
|
||||
<li id="nav_publisher" {% if page == 'publisher' %}class="active"{% endif %}><a href="{{url_for('publisher_list')}}"><span class="glyphicon glyphicon-text-size"></span>{{_('Publishers')}}</a></li>
|
||||
{%endif%}
|
||||
{% if g.user.filter_language() == 'all' and g.user.show_language() %}
|
||||
<li id="nav_lang" {% if page == 'language' %}class="active"{% endif %}><a href="{{url_for('language_overview')}}"><span class="glyphicon glyphicon-flag"></span>{{_('Languages')}} </a></li>
|
||||
{%endif%}
|
||||
{% if g.user.is_authenticated or g.user.is_anonymous %}
|
||||
<li class="nav-head hidden-xs">{{_('Public Shelves')}}</li>
|
||||
<li class="nav-head hidden-xs public-shelves">{{_('Public Shelves')}}</li>
|
||||
{% for shelf in g.public_shelfes %}
|
||||
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list public_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li>
|
||||
{% endfor %}
|
||||
<li class="nav-head hidden-xs">{{_('Your Shelves')}}</li>
|
||||
<li class="nav-head hidden-xs your-shelves">{{_('Your Shelves')}}</li>
|
||||
{% for shelf in g.user.shelf %}
|
||||
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list private_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li>
|
||||
{% endfor %}
|
||||
@ -237,13 +242,26 @@
|
||||
<script src="{{ url_for('static', filename='js/libs/plugins.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/jquery.form.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/uploadprogress.js') }}"> </script>
|
||||
{% if g.current_theme == 1 %}
|
||||
<script src="{{ url_for('static', filename='js/libs/jquery.visible.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/compromise.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/readmore.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/caliBlur.js') }}"></script>
|
||||
{% endif %}
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
$("#btn-upload").change(function () {
|
||||
$("#loader").show();
|
||||
$("#form-upload").submit();
|
||||
});
|
||||
});
|
||||
$(function() {
|
||||
$("#form-upload").uploadprogress({
|
||||
redirect_url: "{{ url_for('index')}}",
|
||||
uploadedMsg: "{{_('Upload done, processing, please wait...')}}",
|
||||
modalTitle: "{{_('Uploading...')}}",
|
||||
modalFooter: "{{_('Close')}}",
|
||||
modalTitleFailed: "{{_('Error')}}"
|
||||
});
|
||||
$("#btn-upload").change(function() {
|
||||
$("#form-upload").submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% block js %}{% endblock %}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<h1>{{title}}</h1>
|
||||
<h1 class="{{page}}">{{_(title)}}</h1>
|
||||
<div class="container">
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
{% for entry in entries %}
|
||||
|
@ -6,7 +6,7 @@
|
||||
<Developer>Janeczku</Developer>
|
||||
<Contact>https://github.com/janeczku/calibre-web</Contact>
|
||||
<Url type="text/html"
|
||||
template="{{url_for('search')}}?query={searchTerms}"/>
|
||||
template="{{url_for('feed_cc_search')}}{searchTerms}"/>
|
||||
<Url type="application/atom+xml"
|
||||
template="{{url_for('feed_normal_search')}}?query={searchTerms}"/>
|
||||
<SyndicationRight>open</SyndicationRight>
|
||||
|
@ -7,6 +7,8 @@
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-icon" sizes="140x140" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/libs/normalize.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
||||
|
@ -41,16 +41,11 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
<!--<link rel="resource" type="application/l10n" href="locale/locale.properties">-->
|
||||
<link rel="resource" type="application/l10n" href="{{ url_for('static', filename='locale/locale.properties') }}">
|
||||
<script src="{{ url_for('static', filename='js/libs/l10n.js') }}"></script>
|
||||
<!--<script src="l10n.js"></script>-->
|
||||
<!--script src="{{ url_for('static', filename='js/libs/debugger.js') }}"></script-->
|
||||
<!--<script src="debugger.js"></script>-->
|
||||
<script src="{{ url_for('static', filename='js/libs/pdf.js') }}"></script>
|
||||
<!--<script src="pdf.js"></script>-->
|
||||
|
||||
<script type="text/javascript">
|
||||
var DEFAULT_URL = "{{ url_for('serve_book', book_id=pdffile, book_format='pdf') }}";
|
||||
var PDFWORKER_LOCATION="{{ url_for('static', filename='js/libs/pdf.worker.js') }}";
|
||||
// var IMAGE_LOCATION="{{ url_for('static', filename='css/../images') }}";
|
||||
var IMAGE_LOCATION="{{ url_for('static', filename='/images/') }}";
|
||||
var PDFWORKER_LOCATION_JS="{{ url_for('static', filename='js/libs/pdf.worker') }}";
|
||||
</script>
|
||||
@ -420,8 +415,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
}
|
||||
</style>
|
||||
<div class="mozPrintCallback-dialog-box">
|
||||
<!-- TODO: Localise the following strings -->
|
||||
Preparing document for printing...
|
||||
{{_('Preparing document for printing...')}}
|
||||
<div class="progress-row">
|
||||
<progress value="0" max="100"></progress>
|
||||
<span class="relative-progress">0%</span>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<h2>{{_('No Results for:')}} {{searchterm}}</h2>
|
||||
<p>{{_('Please try a different search')}}</p>
|
||||
{% else %}
|
||||
<h2>{{entries|length}} {{_('Results for:')}} {{searchterm}}</h2>
|
||||
<h2>{{entries|length}} {{_('Results for:')}} {{searchterm}}</h2>
|
||||
{% if g.user.is_authenticated %}
|
||||
{% if g.user.shelf.all() or g.public_shelfes %}
|
||||
<div id="shelf-actions" class="btn-toolbar" role="toolbar">
|
||||
@ -36,17 +36,29 @@
|
||||
<div class="cover">
|
||||
{% if entry.has_cover is defined %}
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" />
|
||||
<img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="meta">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
</a>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="{{url_for('author', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a>
|
||||
{% if not loop.last %}
|
||||
&
|
||||
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
|
||||
{% if not loop.first %}
|
||||
<span class="author-hidden-divider">&</span>
|
||||
{% endif %}
|
||||
<a class="author-name author-hidden" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% if loop.last %}
|
||||
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if not loop.first %}
|
||||
<span>&</span>
|
||||
{% endif %}
|
||||
<a class="author-name" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
|
@ -141,10 +141,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if c.datatype == 'rating' %}
|
||||
<input type="number" min="1" max="5" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}"
|
||||
{% if book['custom_column_' ~ c.id]|length > 0 %}
|
||||
value="{{ '%d' % (book['custom_column_' ~ c.id][0].value / 2) }}"
|
||||
{% endif %}>
|
||||
<input type="number" min="1" max="5" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@ -16,19 +16,31 @@
|
||||
<div class="cover">
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
{% if entry.has_cover %}
|
||||
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" />
|
||||
<img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
</a>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a>
|
||||
{% if not loop.last %}
|
||||
&
|
||||
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
|
||||
{% if not loop.first %}
|
||||
<span class="author-hidden-divider">&</span>
|
||||
{% endif %}
|
||||
<a class="author-name author-hidden" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% if loop.last %}
|
||||
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if not loop.first %}
|
||||
<span>&</span>
|
||||
{% endif %}
|
||||
<a class="author-name" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
|
@ -11,7 +11,7 @@
|
||||
{% if g.user.role_admin() %}
|
||||
<th data-halign="right" data-align="right" data-field="user" data-sortable="true">{{_('User')}}</th>
|
||||
{% endif %}
|
||||
<th data-halign="right" data-align="right" data-field="type" data-sortable="true">{{_('Task')}}</th>
|
||||
<th data-halign="right" data-align="right" data-field="taskMessage" data-sortable="true">{{_('Task')}}</th>
|
||||
<th data-halign="right" data-align="right" data-field="status" data-sortable="true">{{_('Status')}}</th>
|
||||
<th data-halign="right" data-align="right" data-field="progress" data-sortable="true" data-sorter="elementSorter">{{_('Progress')}}</th>
|
||||
<th data-halign="right" data-align="right" data-field="runtime" data-sortable="true" data-sort-name="rt">{{_('Runtime')}}</th>
|
||||
|
@ -16,7 +16,7 @@
|
||||
{% if ( g.user and g.user.role_passwd() or g.user.role_admin() ) and not content.role_anonymous() %}
|
||||
{% if g.user and g.user.role_admin() and g.allow_registration and not new_user and not profile %}
|
||||
<div class="btn btn-default" id="resend_password"><a href="{{url_for('reset_password', user_id = content.id) }}">{{_('Reset user Password')}}</a></div>
|
||||
{% else %}
|
||||
{% else %}
|
||||
<div class="form-group">
|
||||
<label for="password">{{_('Password')}}</label>
|
||||
<input type="password" class="form-control" name="password" id="password" value="" autocomplete="off">
|
||||
@ -35,14 +35,6 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="theme">{{_('Theme')}}</label>
|
||||
<select name="theme" id="theme" class="form-control">
|
||||
<option value="0" {% if content.get_theme == 0 %}selected{% endif %}>{{ _("Standard Theme") }}</option>
|
||||
<option value="1" {% if content.get_theme == 1 %}selected{% endif %}>{{ _("caliBlur! Dark Theme (Beta)") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="default_language">{{_('Show books with language')}}</label>
|
||||
<select name="default_language" id="default_language" class="form-control">
|
||||
@ -89,10 +81,16 @@
|
||||
<input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}>
|
||||
<label for="show_author">{{_('Show author selection')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="show_publisher" id="show_publisher" {% if content.show_publisher() %}checked{% endif %}>
|
||||
<label for="show_publisher">{{_('Show publisher selection')}}</label>
|
||||
</div>
|
||||
{% if not content.role_anonymous() %}
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}>
|
||||
<label for="show_read_and_unread">{{_('Show read and unread')}}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="show_detail_random" id="show_detail_random" {% if content.show_detail_random() %}checked{% endif %}>
|
||||
<label for="show_detail_random">{{_('Show random books in detail view')}}</label>
|
||||
@ -159,7 +157,7 @@
|
||||
{% for entry in downloads %}
|
||||
<div class="col-sm-2">
|
||||
<a class="pull-left" href="{{ url_for('show_book', book_id=entry.id) }}">
|
||||
<img class="media-object" width="100" src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="...">
|
||||
<img class="media-object" width="100" src="{{ url_for('get_cover', book_id=entry.id) }}" alt="...">
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
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
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
BIN
cps/translations/sv/LC_MESSAGES/messages.mo
Normal file
BIN
cps/translations/sv/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
2061
cps/translations/sv/LC_MESSAGES/messages.po
Normal file
2061
cps/translations/sv/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
cps/translations/uk/LC_MESSAGES/messages.mo
Normal file
BIN
cps/translations/uk/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
2156
cps/translations/uk/LC_MESSAGES/messages.po
Normal file
2156
cps/translations/uk/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
91
cps/ub.py
91
cps/ub.py
@ -1,6 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2012-2019 mutschler, jkrehm, cervinko, janeczku, OzzieIsaacs, csitko
|
||||
# ok11, issmirnov, idalin
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy import exc
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
@ -41,10 +58,20 @@ SIDEBAR_READ_AND_UNREAD = 256
|
||||
SIDEBAR_RECENT = 512
|
||||
SIDEBAR_SORTED = 1024
|
||||
MATURE_CONTENT = 2048
|
||||
SIDEBAR_PUBLISHER = 4096
|
||||
|
||||
DEFAULT_PASS = "admin123"
|
||||
DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083))
|
||||
try:
|
||||
DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083))
|
||||
except ValueError:
|
||||
print ('Environmentvariable CALIBRE_PORT is set to an invalid value: ' +
|
||||
os.environ.get("CALIBRE_PORT", 8083) + ', faling back to default (8083)')
|
||||
DEFAULT_PORT = 8083
|
||||
|
||||
UPDATE_STABLE = 0
|
||||
AUTO_UPDATE_STABLE = 1
|
||||
UPDATE_NIGHTLY = 2
|
||||
AUTO_UPDATE_NIGHTLY = 4
|
||||
|
||||
class UserBase:
|
||||
|
||||
@ -102,10 +129,6 @@ class UserBase:
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def get_theme(self):
|
||||
return self.theme
|
||||
|
||||
def get_id(self):
|
||||
return str(self.id)
|
||||
|
||||
@ -136,6 +159,9 @@ class UserBase:
|
||||
def show_author(self):
|
||||
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_AUTHOR == SIDEBAR_AUTHOR))
|
||||
|
||||
def show_publisher(self):
|
||||
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_PUBLISHER == SIDEBAR_PUBLISHER))
|
||||
|
||||
def show_best_rated_books(self):
|
||||
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_BEST_RATED == SIDEBAR_BEST_RATED))
|
||||
|
||||
@ -166,7 +192,6 @@ class User(UserBase, Base):
|
||||
sidebar_view = Column(Integer, default=1)
|
||||
default_language = Column(String(3), default="all")
|
||||
mature_content = Column(Boolean, default=True)
|
||||
theme = Column(Integer, default=0)
|
||||
|
||||
|
||||
# Class for anonymous user is derived from User base and completly overrides methods and properties for the
|
||||
@ -259,7 +284,7 @@ class Downloads(Base):
|
||||
def __repr__(self):
|
||||
return '<Download %r' % self.book_id
|
||||
|
||||
|
||||
|
||||
# Baseclass representing allowed domains for registration
|
||||
class Registration(Base):
|
||||
__tablename__ = 'registration'
|
||||
@ -268,8 +293,8 @@ class Registration(Base):
|
||||
domain = Column(String)
|
||||
|
||||
def __repr__(self):
|
||||
return u"<Registration('{0}')>".format(self.domain)
|
||||
|
||||
return u"<Registration('{0}')>".format(self.domain)
|
||||
|
||||
|
||||
# Baseclass for representing settings in app.db with email server settings and Calibre database settings
|
||||
# (application settings)
|
||||
@ -290,6 +315,7 @@ class Settings(Base):
|
||||
config_calibre_web_title = Column(String, default=u'Calibre-Web')
|
||||
config_books_per_page = Column(Integer, default=60)
|
||||
config_random_books = Column(Integer, default=4)
|
||||
config_authors_max = Column(Integer, default=0)
|
||||
config_read_column = Column(Integer, default=0)
|
||||
config_title_regex = Column(String, default=u'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
|
||||
config_log_level = Column(SmallInteger, default=logging.INFO)
|
||||
@ -297,7 +323,7 @@ class Settings(Base):
|
||||
config_anonbrowse = Column(SmallInteger, default=0)
|
||||
config_public_reg = Column(SmallInteger, default=0)
|
||||
config_default_role = Column(SmallInteger, default=0)
|
||||
config_default_show = Column(SmallInteger, default=2047)
|
||||
config_default_show = Column(SmallInteger, default=6143)
|
||||
config_columns_to_ignore = Column(String)
|
||||
config_use_google_drive = Column(Boolean)
|
||||
config_google_drive_folder = Column(String)
|
||||
@ -312,6 +338,8 @@ class Settings(Base):
|
||||
config_converterpath = Column(String)
|
||||
config_calibre = Column(String)
|
||||
config_rarfile_location = Column(String)
|
||||
config_theme = Column(Integer, default=0)
|
||||
config_updatechannel = Column(Integer, default=0)
|
||||
|
||||
def __repr__(self):
|
||||
pass
|
||||
@ -353,6 +381,7 @@ class Config:
|
||||
self.config_calibre_web_title = data.config_calibre_web_title
|
||||
self.config_books_per_page = data.config_books_per_page
|
||||
self.config_random_books = data.config_random_books
|
||||
self.config_authors_max = data.config_authors_max
|
||||
self.config_title_regex = data.config_title_regex
|
||||
self.config_read_column = data.config_read_column
|
||||
self.config_log_level = data.config_log_level
|
||||
@ -385,11 +414,17 @@ class Config:
|
||||
if data.config_logfile:
|
||||
self.config_logfile = data.config_logfile
|
||||
self.config_rarfile_location = data.config_rarfile_location
|
||||
self.config_theme = data.config_theme
|
||||
self.config_updatechannel = data.config_updatechannel
|
||||
|
||||
@property
|
||||
def get_main_dir(self):
|
||||
return self.config_main_dir
|
||||
|
||||
@property
|
||||
def get_update_channel(self):
|
||||
return self.config_updatechannel
|
||||
|
||||
def get_config_certfile(self):
|
||||
if cli.certfilepath:
|
||||
return cli.certfilepath
|
||||
@ -485,6 +520,10 @@ class Config:
|
||||
return bool((self.config_default_show is not None) and
|
||||
(self.config_default_show & SIDEBAR_AUTHOR == SIDEBAR_AUTHOR))
|
||||
|
||||
def show_publisher(self):
|
||||
return bool((self.config_default_show is not None) and
|
||||
(self.config_default_show & SIDEBAR_PUBLISHER == SIDEBAR_PUBLISHER))
|
||||
|
||||
def show_best_rated_books(self):
|
||||
return bool((self.config_default_show is not None) and
|
||||
(self.config_default_show & SIDEBAR_BEST_RATED == SIDEBAR_BEST_RATED))
|
||||
@ -529,6 +568,8 @@ class Config:
|
||||
# everywhere to curent should work. Migration is done by checking if relevant coloums are existing, and than adding
|
||||
# rows with SQL commands
|
||||
def migrate_Database():
|
||||
if not engine.dialect.has_table(engine.connect(), "book_read_link"):
|
||||
ReadBook.__table__.create(bind=engine)
|
||||
if not engine.dialect.has_table(engine.connect(), "bookmark"):
|
||||
Bookmark.__table__.create(bind=engine)
|
||||
if not engine.dialect.has_table(engine.connect(), "registration"):
|
||||
@ -549,7 +590,7 @@ def migrate_Database():
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_use_google_drive` INTEGER DEFAULT 0")
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_folder` String DEFAULT ''")
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_watch_changes_response` String DEFAULT ''")
|
||||
session.commit()
|
||||
session.commit()
|
||||
try:
|
||||
session.query(exists().where(Settings.config_columns_to_ignore)).scalar()
|
||||
except exc.OperationalError:
|
||||
@ -562,6 +603,12 @@ def migrate_Database():
|
||||
conn = engine.connect()
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_default_role` SmallInteger DEFAULT 0")
|
||||
session.commit()
|
||||
try:
|
||||
session.query(exists().where(Settings.config_authors_max)).scalar()
|
||||
except exc.OperationalError: # Database is not compatible, some rows are missing
|
||||
conn = engine.connect()
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_authors_max` INTEGER DEFAULT 0")
|
||||
session.commit()
|
||||
try:
|
||||
session.query(exists().where(BookShelf.order)).scalar()
|
||||
except exc.OperationalError: # Database is not compatible, some rows are missing
|
||||
@ -602,12 +649,7 @@ def migrate_Database():
|
||||
except exc.OperationalError:
|
||||
conn = engine.connect()
|
||||
conn.execute("ALTER TABLE user ADD column `mature_content` INTEGER DEFAULT 1")
|
||||
try:
|
||||
session.query(exists().where(User.theme)).scalar()
|
||||
except exc.OperationalError:
|
||||
conn = engine.connect()
|
||||
conn.execute("ALTER TABLE user ADD column `theme` INTEGER DEFAULT 0")
|
||||
session.commit()
|
||||
|
||||
if session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() is None:
|
||||
create_anonymous_user()
|
||||
try:
|
||||
@ -660,6 +702,19 @@ def migrate_Database():
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_converterpath` String DEFAULT ''")
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_calibre` String DEFAULT ''")
|
||||
session.commit()
|
||||
try:
|
||||
session.query(exists().where(Settings.config_theme)).scalar()
|
||||
except exc.OperationalError: # Database is not compatible, some rows are missing
|
||||
conn = engine.connect()
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_theme` INTEGER DEFAULT 0")
|
||||
session.commit()
|
||||
try:
|
||||
session.query(exists().where(Settings.config_updatechannel)).scalar()
|
||||
except exc.OperationalError: # Database is not compatible, some rows are missing
|
||||
conn = engine.connect()
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_updatechannel` INTEGER DEFAULT 0")
|
||||
session.commit()
|
||||
|
||||
|
||||
# Remove login capability of user Guest
|
||||
conn = engine.connect()
|
||||
@ -740,7 +795,7 @@ def create_admin_user():
|
||||
user.role = ROLE_USER + ROLE_ADMIN + ROLE_DOWNLOAD + ROLE_UPLOAD + ROLE_EDIT + ROLE_DELETE_BOOKS + ROLE_PASSWD
|
||||
user.sidebar_view = DETAIL_RANDOM + SIDEBAR_LANGUAGE + SIDEBAR_SERIES + SIDEBAR_CATEGORY + SIDEBAR_HOT + \
|
||||
SIDEBAR_RANDOM + SIDEBAR_AUTHOR + SIDEBAR_BEST_RATED + SIDEBAR_READ_AND_UNREAD + SIDEBAR_RECENT + \
|
||||
SIDEBAR_SORTED + MATURE_CONTENT
|
||||
SIDEBAR_SORTED + MATURE_CONTENT + SIDEBAR_PUBLISHER
|
||||
|
||||
user.password = generate_password_hash(DEFAULT_PASS)
|
||||
|
||||
|
514
cps/updater.py
Normal file
514
cps/updater.py
Normal file
@ -0,0 +1,514 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2018-2019 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import threading
|
||||
import zipfile
|
||||
import requests
|
||||
import re
|
||||
import logging
|
||||
import server
|
||||
import time
|
||||
from io import BytesIO
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
from ub import config, UPDATE_STABLE
|
||||
from tempfile import gettempdir
|
||||
import datetime
|
||||
import json
|
||||
from flask_babel import gettext as _
|
||||
from babel.dates import format_datetime
|
||||
import web
|
||||
|
||||
|
||||
def is_sha1(sha1):
|
||||
if len(sha1) != 40:
|
||||
return False
|
||||
try:
|
||||
int(sha1, 16)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class Updater(threading.Thread):
|
||||
|
||||
def __init__(self):
|
||||
threading.Thread.__init__(self)
|
||||
self.status = -1
|
||||
self.updateIndex = None
|
||||
|
||||
def get_current_version_info(self):
|
||||
if config.get_update_channel == UPDATE_STABLE:
|
||||
return self._stable_version_info()
|
||||
else:
|
||||
return self._nightly_version_info()
|
||||
|
||||
def get_available_updates(self, request_method):
|
||||
if config.get_update_channel == UPDATE_STABLE:
|
||||
return self._stable_available_updates(request_method)
|
||||
else:
|
||||
return self._nightly_available_updates(request_method)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.status = 1
|
||||
r = requests.get(self._get_request_path(), stream=True)
|
||||
r.raise_for_status()
|
||||
|
||||
self.status = 2
|
||||
z = zipfile.ZipFile(BytesIO(r.content))
|
||||
self.status = 3
|
||||
tmp_dir = gettempdir()
|
||||
z.extractall(tmp_dir)
|
||||
foldername = os.path.join(tmp_dir, z.namelist()[0])[:-1]
|
||||
if not os.path.isdir(foldername):
|
||||
self.status = 11
|
||||
logging.getLogger('cps.web').info(u'Extracted contents of zipfile not found in temp folder')
|
||||
return
|
||||
self.status = 4
|
||||
self.update_source(foldername, config.get_main_dir)
|
||||
self.status = 6
|
||||
time.sleep(2)
|
||||
server.Server.setRestartTyp(True)
|
||||
server.Server.stopServer()
|
||||
self.status = 7
|
||||
time.sleep(2)
|
||||
except requests.exceptions.HTTPError as ex:
|
||||
logging.getLogger('cps.web').info( u'HTTP Error' + ' ' + str(ex))
|
||||
self.status = 8
|
||||
except requests.exceptions.ConnectionError:
|
||||
logging.getLogger('cps.web').info(u'Connection error')
|
||||
self.status = 9
|
||||
except requests.exceptions.Timeout:
|
||||
logging.getLogger('cps.web').info(u'Timeout while establishing connection')
|
||||
self.status = 10
|
||||
except requests.exceptions.RequestException:
|
||||
self.status = 11
|
||||
logging.getLogger('cps.web').info(u'General error')
|
||||
|
||||
def get_update_status(self):
|
||||
return self.status
|
||||
|
||||
@classmethod
|
||||
def file_to_list(self, filelist):
|
||||
return [x.strip() for x in open(filelist, 'r') if not x.startswith('#EXT')]
|
||||
|
||||
@classmethod
|
||||
def one_minus_two(self, one, two):
|
||||
return [x for x in one if x not in set(two)]
|
||||
|
||||
@classmethod
|
||||
def reduce_dirs(self, delete_files, new_list):
|
||||
new_delete = []
|
||||
for filename in delete_files:
|
||||
parts = filename.split(os.sep)
|
||||
sub = ''
|
||||
for part in parts:
|
||||
sub = os.path.join(sub, part)
|
||||
if sub == '':
|
||||
sub = os.sep
|
||||
count = 0
|
||||
for song in new_list:
|
||||
if song.startswith(sub):
|
||||
count += 1
|
||||
break
|
||||
if count == 0:
|
||||
if sub != '\\':
|
||||
new_delete.append(sub)
|
||||
break
|
||||
return list(set(new_delete))
|
||||
|
||||
@classmethod
|
||||
def reduce_files(self, remove_items, exclude_items):
|
||||
rf = []
|
||||
for item in remove_items:
|
||||
if not item.startswith(exclude_items):
|
||||
rf.append(item)
|
||||
return rf
|
||||
|
||||
@classmethod
|
||||
def moveallfiles(self, root_src_dir, root_dst_dir):
|
||||
change_permissions = True
|
||||
if sys.platform == "win32" or sys.platform == "darwin":
|
||||
change_permissions = False
|
||||
else:
|
||||
logging.getLogger('cps.web').debug('Update on OS-System : ' + sys.platform)
|
||||
new_permissions = os.stat(root_dst_dir)
|
||||
# print new_permissions
|
||||
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):
|
||||
os.makedirs(dst_dir)
|
||||
logging.getLogger('cps.web').debug('Create-Dir: '+dst_dir)
|
||||
if change_permissions:
|
||||
# print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid))
|
||||
os.chown(dst_dir, new_permissions.st_uid, new_permissions.st_gid)
|
||||
for file_ in files:
|
||||
src_file = os.path.join(src_dir, file_)
|
||||
dst_file = os.path.join(dst_dir, file_)
|
||||
if os.path.exists(dst_file):
|
||||
if change_permissions:
|
||||
permission = os.stat(dst_file)
|
||||
logging.getLogger('cps.web').debug('Remove file before copy: '+dst_file)
|
||||
os.remove(dst_file)
|
||||
else:
|
||||
if change_permissions:
|
||||
permission = new_permissions
|
||||
shutil.move(src_file, dst_dir)
|
||||
logging.getLogger('cps.web').debug('Move File '+src_file+' to '+dst_dir)
|
||||
if change_permissions:
|
||||
try:
|
||||
os.chown(dst_file, permission.st_uid, permission.st_gid)
|
||||
except (Exception) as e:
|
||||
# ex = sys.exc_info()
|
||||
old_permissions = os.stat(dst_file)
|
||||
logging.getLogger('cps.web').debug('Fail change permissions of ' + str(dst_file) + '. Before: '
|
||||
+ str(old_permissions.st_uid) + ':' + str(old_permissions.st_gid) + ' After: '
|
||||
+ str(permission.st_uid) + ':' + str(permission.st_gid) + ' error: '+str(e))
|
||||
return
|
||||
|
||||
def update_source(self, source, destination):
|
||||
# destination files
|
||||
old_list = list()
|
||||
exclude = (
|
||||
os.sep + 'app.db', os.sep + 'calibre-web.log1', os.sep + 'calibre-web.log2', os.sep + 'gdrive.db',
|
||||
os.sep + 'vendor', os.sep + 'calibre-web.log', os.sep + '.git', os.sep +'client_secrets.json',
|
||||
os.sep + 'gdrive_credentials', os.sep + 'settings.yaml')
|
||||
for root, dirs, files in os.walk(destination, topdown=True):
|
||||
for name in files:
|
||||
old_list.append(os.path.join(root, name).replace(destination, ''))
|
||||
for name in dirs:
|
||||
old_list.append(os.path.join(root, name).replace(destination, ''))
|
||||
# source files
|
||||
new_list = list()
|
||||
for root, dirs, files in os.walk(source, topdown=True):
|
||||
for name in files:
|
||||
new_list.append(os.path.join(root, name).replace(source, ''))
|
||||
for name in dirs:
|
||||
new_list.append(os.path.join(root, name).replace(source, ''))
|
||||
|
||||
delete_files = self.one_minus_two(old_list, new_list)
|
||||
|
||||
rf = self.reduce_files(delete_files, exclude)
|
||||
|
||||
remove_items = self.reduce_dirs(rf, new_list)
|
||||
|
||||
self.moveallfiles(source, destination)
|
||||
|
||||
for item in remove_items:
|
||||
item_path = os.path.join(destination, item[1:])
|
||||
if os.path.isdir(item_path):
|
||||
logging.getLogger('cps.web').debug("Delete dir " + item_path)
|
||||
shutil.rmtree(item_path, ignore_errors=True)
|
||||
else:
|
||||
try:
|
||||
logging.getLogger('cps.web').debug("Delete file " + item_path)
|
||||
# log_from_thread("Delete file " + item_path)
|
||||
os.remove(item_path)
|
||||
except Exception:
|
||||
logging.getLogger('cps.web').debug("Could not remove:" + item_path)
|
||||
shutil.rmtree(source, ignore_errors=True)
|
||||
|
||||
def _nightly_version_info(self):
|
||||
content = {}
|
||||
content[0] = '$Format:%H$'
|
||||
content[1] = '$Format:%cI$'
|
||||
# content[0] = 'bb7d2c6273ae4560e83950d36d64533343623a57'
|
||||
# content[1] = '2018-09-09T10:13:08+02:00'
|
||||
if is_sha1(content[0]) and len(content[1]) > 0:
|
||||
return {'version': content[0], 'datetime': content[1]}
|
||||
return False
|
||||
|
||||
def _stable_version_info(self):
|
||||
return {'version': '0.6.1'} # Current version
|
||||
|
||||
def _nightly_available_updates(self, request_method):
|
||||
tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
|
||||
if request_method == "GET":
|
||||
repository_url = 'https://api.github.com/repos/janeczku/calibre-web'
|
||||
status, commit = self._load_remote_data(repository_url +'/git/refs/heads/master')
|
||||
parents = []
|
||||
if status['message'] != '':
|
||||
return json.dumps(status)
|
||||
if 'object' not in commit:
|
||||
status['message'] = _(u'Unexpected data while reading update information')
|
||||
return json.dumps(status)
|
||||
|
||||
if commit['object']['sha'] == status['current_commit_hash']:
|
||||
status.update({
|
||||
'update': False,
|
||||
'success': True,
|
||||
'message': _(u'No update available. You already have the latest version installed')
|
||||
})
|
||||
return json.dumps(status)
|
||||
|
||||
# a new update is available
|
||||
status['update'] = True
|
||||
|
||||
try:
|
||||
r = requests.get(repository_url + '/git/commits/' + commit['object']['sha'])
|
||||
r.raise_for_status()
|
||||
update_data = r.json()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
status['error'] = _(u'HTTP Error') + ' ' + str(e)
|
||||
except requests.exceptions.ConnectionError:
|
||||
status['error'] = _(u'Connection error')
|
||||
except requests.exceptions.Timeout:
|
||||
status['error'] = _(u'Timeout while establishing connection')
|
||||
except requests.exceptions.RequestException:
|
||||
status['error'] = _(u'General error')
|
||||
|
||||
if status['message'] != '':
|
||||
return json.dumps(status)
|
||||
|
||||
if 'committer' in update_data and 'message' in update_data:
|
||||
status['success'] = True
|
||||
status['message'] = _(
|
||||
u'A new update is available. Click on the button below to update to the latest version.')
|
||||
|
||||
new_commit_date = datetime.datetime.strptime(
|
||||
update_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
|
||||
parents.append(
|
||||
[
|
||||
format_datetime(new_commit_date, format='short', locale=web.get_locale()),
|
||||
update_data['message'],
|
||||
update_data['sha']
|
||||
]
|
||||
)
|
||||
|
||||
# it only makes sense to analyze the parents if we know the current commit hash
|
||||
if status['current_commit_hash'] != '':
|
||||
try:
|
||||
parent_commit = update_data['parents'][0]
|
||||
# limit the maximum search depth
|
||||
remaining_parents_cnt = 10
|
||||
except IndexError:
|
||||
remaining_parents_cnt = None
|
||||
|
||||
if remaining_parents_cnt is not None:
|
||||
while True:
|
||||
if remaining_parents_cnt == 0:
|
||||
break
|
||||
|
||||
# check if we are more than one update behind if so, go up the tree
|
||||
if parent_commit['sha'] != status['current_commit_hash']:
|
||||
try:
|
||||
r = requests.get(parent_commit['url'])
|
||||
r.raise_for_status()
|
||||
parent_data = r.json()
|
||||
|
||||
parent_commit_date = datetime.datetime.strptime(
|
||||
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
|
||||
parent_commit_date = format_datetime(
|
||||
parent_commit_date, format='short', locale=web.get_locale())
|
||||
|
||||
parents.append([parent_commit_date,
|
||||
parent_data['message'].replace('\r\n','<p>').replace('\n','<p>')])
|
||||
parent_commit = parent_data['parents'][0]
|
||||
remaining_parents_cnt -= 1
|
||||
except Exception:
|
||||
# it isn't crucial if we can't get information about the parent
|
||||
break
|
||||
else:
|
||||
# parent is our current version
|
||||
break
|
||||
|
||||
else:
|
||||
status['success'] = False
|
||||
status['message'] = _(u'Could not fetch update information')
|
||||
|
||||
# a new update is available
|
||||
status['update'] = True
|
||||
if 'body' in commit:
|
||||
status['success'] = True
|
||||
status['message'] = _(
|
||||
u'A new update is available. Click on the button below to update to the latest version.')
|
||||
|
||||
new_commit_date = datetime.datetime.strptime(
|
||||
commit['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
|
||||
parents.append(
|
||||
[
|
||||
format_datetime(new_commit_date, format='short', locale=web.get_locale()),
|
||||
commit['message'],
|
||||
commit['sha']
|
||||
]
|
||||
)
|
||||
|
||||
# it only makes sense to analyze the parents if we know the current commit hash
|
||||
if status['current_commit_hash'] != '':
|
||||
try:
|
||||
parent_commit = commit['parents'][0]
|
||||
# limit the maximum search depth
|
||||
remaining_parents_cnt = 10
|
||||
except IndexError:
|
||||
remaining_parents_cnt = None
|
||||
|
||||
if remaining_parents_cnt is not None:
|
||||
while True:
|
||||
if remaining_parents_cnt == 0:
|
||||
break
|
||||
|
||||
# check if we are more than one update behind if so, go up the tree
|
||||
if commit['sha'] != status['current_commit_hash']:
|
||||
try:
|
||||
r = requests.get(parent_commit['url'])
|
||||
r.raise_for_status()
|
||||
parent_data = r.json()
|
||||
|
||||
parent_commit_date = datetime.datetime.strptime(
|
||||
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
|
||||
parent_commit_date = format_datetime(
|
||||
parent_commit_date, format='short', locale=web.get_locale())
|
||||
|
||||
parents.append([parent_commit_date, parent_data['message'], parent_data['sha']])
|
||||
parent_commit = parent_data['parents'][0]
|
||||
remaining_parents_cnt -= 1
|
||||
except Exception:
|
||||
# it isn't crucial if we can't get information about the parent
|
||||
break
|
||||
else:
|
||||
# parent is our current version
|
||||
break
|
||||
status['history'] = parents[::-1]
|
||||
return json.dumps(status)
|
||||
return ''
|
||||
|
||||
def _stable_available_updates(self, request_method):
|
||||
if request_method == "GET":
|
||||
parents = []
|
||||
# repository_url = 'https://api.github.com/repos/flatpak/flatpak/releases' # test URL
|
||||
repository_url = 'https://api.github.com/repos/janeczku/calibre-web/releases'
|
||||
status, commit = self._load_remote_data(repository_url)
|
||||
if status['message'] != '':
|
||||
return json.dumps(status)
|
||||
if not commit:
|
||||
status['success'] = True
|
||||
status['message'] = _(u'No release information available')
|
||||
return json.dumps(status)
|
||||
version = status['current_commit_hash']
|
||||
current_version = status['current_commit_hash'].split('.')
|
||||
|
||||
# we are already on newest version, no update available
|
||||
if 'tag_name' not in commit[0]:
|
||||
status['message'] = _(u'Unexpected data while reading update information')
|
||||
return json.dumps(status)
|
||||
if commit[0]['tag_name'] == version:
|
||||
status.update({
|
||||
'update': False,
|
||||
'success': True,
|
||||
'message': _(u'No update available. You already have the latest version installed')
|
||||
})
|
||||
return json.dumps(status)
|
||||
|
||||
i = len(commit) - 1
|
||||
while i >= 0:
|
||||
if 'tag_name' not in commit[i] or 'body' not in commit[i]:
|
||||
status['message'] = _(u'Unexpected data while reading update information')
|
||||
return json.dumps(status)
|
||||
major_version_update = int(commit[i]['tag_name'].split('.')[0])
|
||||
minor_version_update = int(commit[i]['tag_name'].split('.')[1])
|
||||
patch_version_update = int(commit[i]['tag_name'].split('.')[2])
|
||||
|
||||
# Check if major versions are identical search for newest nonenqual commit and update to this one
|
||||
if major_version_update == int(current_version[0]):
|
||||
if (minor_version_update == int(current_version[1]) and
|
||||
patch_version_update > int(current_version[2])) or \
|
||||
minor_version_update > int(current_version[1]):
|
||||
parents.append([commit[i]['tag_name'],commit[i]['body'].replace('\r\n', '<p>')])
|
||||
i -= 1
|
||||
continue
|
||||
if major_version_update < int(current_version[0]):
|
||||
i -= 1
|
||||
continue
|
||||
if major_version_update > int(current_version[0]):
|
||||
# found update update to last version before major update, unless current version is on last version
|
||||
# before major update
|
||||
if commit[i+1]['tag_name'].split('.')[1] == current_version[1]:
|
||||
parents.append([commit[i]['tag_name'],
|
||||
commit[i]['body'].replace('\r\n', '<p>').replace('\n', '<p>')])
|
||||
status.update({
|
||||
'update': True,
|
||||
'success': True,
|
||||
'message': _(u'A new update is available. Click on the button below to '
|
||||
u'update to version: %(version)s', version=commit[i]['tag_name']),
|
||||
'history': parents
|
||||
})
|
||||
self.updateFile = commit[i]['zipball_url']
|
||||
else:
|
||||
status.update({
|
||||
'update': True,
|
||||
'success': True,
|
||||
'message': _(u'A new update is available. Click on the button below to '
|
||||
u'update to version: %(version)s', version=commit[i]['tag_name']),
|
||||
'history': parents
|
||||
})
|
||||
self.updateFile = commit[i +1]['zipball_url']
|
||||
break
|
||||
if i == -1:
|
||||
status.update({
|
||||
'update': True,
|
||||
'success': True,
|
||||
'message': _(
|
||||
u'A new update is available. Click on the button below to update to the latest version.'),
|
||||
'history': parents
|
||||
})
|
||||
self.updateFile = commit[0]['zipball_url']
|
||||
return json.dumps(status)
|
||||
|
||||
def _get_request_path(self):
|
||||
if config.get_update_channel == UPDATE_STABLE:
|
||||
return self.updateFile
|
||||
else:
|
||||
return 'https://api.github.com/repos/janeczku/calibre-web/zipball/master'
|
||||
|
||||
def _load_remote_data(self, repository_url):
|
||||
status = {
|
||||
'update': False,
|
||||
'success': False,
|
||||
'message': '',
|
||||
'current_commit_hash': ''
|
||||
}
|
||||
commit = None
|
||||
version = self.get_current_version_info()
|
||||
if version is False:
|
||||
status['current_commit_hash'] = _(u'Unknown')
|
||||
else:
|
||||
status['current_commit_hash'] = version['version']
|
||||
try:
|
||||
r = requests.get(repository_url)
|
||||
commit = r.json()
|
||||
r.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if commit:
|
||||
if 'message' in commit:
|
||||
status['message'] = _(u'HTTP Error') + ': ' + commit['message']
|
||||
else:
|
||||
status['message'] = _(u'HTTP Error') + ': ' + str(e)
|
||||
except requests.exceptions.ConnectionError:
|
||||
status['message'] = _(u'Connection error')
|
||||
except requests.exceptions.Timeout:
|
||||
status['message'] = _(u'Timeout while establishing connection')
|
||||
except requests.exceptions.RequestException:
|
||||
status['message'] = _(u'General error')
|
||||
|
||||
return status, commit
|
||||
|
||||
|
||||
updater_thread = Updater()
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user