diff --git a/.gitignore b/.gitignore index 15411480..09bf3faa 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ settings.yaml gdrive_credentials vendor +client_secrets.json diff --git a/cps.py b/cps.py index 523246b2..055c0ffe 100755 --- a/cps.py +++ b/cps.py @@ -10,33 +10,12 @@ sys.path.append(base_path) sys.path.append(os.path.join(base_path, 'cps')) sys.path.append(os.path.join(base_path, 'vendor')) -from cps import web -try: - from gevent.wsgi import WSGIServer - gevent_present = True -except ImportError: - from tornado.wsgi import WSGIContainer - from tornado.httpserver import HTTPServer - from tornado.ioloop import IOLoop - gevent_present = False +from cps.server import Server if __name__ == '__main__': - if web.ub.DEVELOPMENT: - web.app.run(port=web.ub.config.config_port, debug=True) - else: - if gevent_present: - web.app.logger.info('Attempting to start gevent') - web.start_gevent() - else: - web.app.logger.info('Falling back to Tornado') - http_server = HTTPServer(WSGIContainer(web.app)) - http_server.listen(web.ub.config.config_port) - IOLoop.instance().start() - IOLoop.instance().close(True) - - if web.helper.global_task == 0: - web.app.logger.info("Performing restart of Calibre-web") - os.execl(sys.executable, sys.executable, *sys.argv) - else: - web.app.logger.info("Performing shutdown of Calibre-web") - sys.exit(0) + Server.startServer() + + + + + diff --git a/cps/book_formats.py b/cps/book_formats.py index 5acbc4c4..1e0a08bd 100644 --- a/cps/book_formats.py +++ b/cps/book_formats.py @@ -5,6 +5,12 @@ import logging import uploader import os from flask_babel import gettext as _ +import comic + +try: + from lxml.etree import LXML_VERSION as lxmlversion +except ImportError: + lxmlversion = None __author__ = 'lemmsh' @@ -14,7 +20,7 @@ try: from wand.image import Image from wand import version as ImageVersion use_generic_pdf_cover = False -except ImportError as e: +except (ImportError, RuntimeError) as e: logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e) use_generic_pdf_cover = True try: @@ -49,8 +55,11 @@ def process(tmp_file_path, original_file_name, original_file_extension): meta = epub.get_epub_info(tmp_file_path, original_file_name, original_file_extension) if ".FB2" == original_file_extension.upper() and use_fb2_meta is True: meta = fb2.get_fb2_info(tmp_file_path, original_file_extension) - except Exception as e: - logger.warning('cannot parse metadata, using default: %s', e) + if original_file_extension.upper() in ['.CBZ', '.CBT']: + meta = comic.get_comic_info(tmp_file_path, original_file_name, original_file_extension) + + except Exception as ex: + logger.warning('cannot parse metadata, using default: %s', ex) if meta and meta.title.strip() and meta.author.strip(): return meta @@ -111,13 +120,18 @@ def pdf_preview(tmp_file_path, tmp_dir): img.save(filename=os.path.join(tmp_dir, cover_file_name)) return cover_file_name + def get_versions(): if not use_generic_pdf_cover: IVersion=ImageVersion.MAGICK_VERSION else: - IVersion=_(u'not installed') + IVersion = _(u'not installed') if use_pdf_meta: - PVersion=PyPdfVersion + PVersion='v'+PyPdfVersion else: PVersion=_(u'not installed') - return {'ImageVersion':IVersion,'PyPdfVersion':PVersion} + if lxmlversion: + XVersion = 'v'+'.'.join(map(str, lxmlversion)) + else: + XVersion = _(u'not installed') + return {'Image Magick': IVersion, 'PyPdf': PVersion, 'lxml':XVersion} diff --git a/cps/cli.py b/cps/cli.py new file mode 100644 index 00000000..5e172626 --- /dev/null +++ b/cps/cli.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import argparse +import os +import sys + +parser = argparse.ArgumentParser(description='Calibre Web is a web app' + ' providing a interface for browsing, reading and downloading eBooks\n', prog='cps.py') +parser.add_argument('-p', metavar='path', help='path and name to settings db, e.g. /opt/cw.db') +parser.add_argument('-g', metavar='path', help='path and name to gdrive db, e.g. /opt/gd.db') +parser.add_argument('-c', metavar='path', help='path and name to SSL certfile, e.g. /opt/test.cert, works only in combination with keyfile') +parser.add_argument('-k', metavar='path', help='path and name to SSL keyfile, e.g. /opt/test.key, works only in combination with certfile') +args = parser.parse_args() + +generalPath = os.path.normpath(os.getenv("CALIBRE_DBPATH", + os.path.dirname(os.path.realpath(__file__)) + os.sep + ".." + os.sep)) +if args.p: + settingspath = args.p +else: + settingspath = os.path.join(generalPath, "app.db") + +if args.g: + gdpath = args.g +else: + gdpath = os.path.join(generalPath, "gdrive.db") + +certfilepath = None +keyfilepath = None +if args.c: + if os.path.isfile(args.c): + certfilepath = args.c + else: + print("Certfilepath is invalid. Exiting...") + sys.exit(1) + +if args.c is "": + certfilepath = "" + +if args.k: + if os.path.isfile(args.k): + keyfilepath = args.k + else: + print("Keyfilepath is invalid. Exiting...") + sys.exit(1) + +if args.k is "": + keyfilepath = "" diff --git a/cps/comic.py b/cps/comic.py new file mode 100644 index 00000000..98343ae7 --- /dev/null +++ b/cps/comic.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import zipfile +import tarfile +import os +import uploader + + +def extractCover(tmp_file_name, original_file_extension): + if original_file_extension.upper() == '.CBZ': + cf = zipfile.ZipFile(tmp_file_name) + compressed_name = cf.namelist()[0] + cover_data = cf.read(compressed_name) + elif original_file_extension.upper() == '.CBT': + cf = tarfile.TarFile(tmp_file_name) + compressed_name = cf.getnames()[0] + cover_data = cf.extractfile(compressed_name).read() + + 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() + return tmp_cover_name + + +def get_comic_info(tmp_file_path, original_file_name, original_file_extension): + + coverfile = extractCover(tmp_file_path, original_file_extension) + + return uploader.BookMeta( + file_path=tmp_file_path, + extension=original_file_extension, + title=original_file_name, + author=u"Unknown", + cover=coverfile, + description="", + tags="", + series="", + series_id="", + languages="") diff --git a/cps/converter.py b/cps/converter.py new file mode 100644 index 00000000..8967d3e5 --- /dev/null +++ b/cps/converter.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import os +import subprocess +import ub +import re +from flask_babel import gettext as _ + + +def versionKindle(): + versions = _(u'not installed') + if os.path.exists(ub.config.config_converterpath): + try: + p = subprocess.Popen(ub.config.config_converterpath, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p.wait() + for lines in p.stdout.readlines(): + if isinstance(lines, bytes): + lines = lines.decode('utf-8') + if re.search('Amazon kindlegen\(', lines): + versions = lines + except Exception: + versions = _(u'Excecution permissions missing') + return {'kindlegen' : versions} + + +def versionCalibre(): + versions = _(u'not installed') + if os.path.exists(ub.config.config_converterpath): + try: + p = subprocess.Popen([ub.config.config_converterpath, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p.wait() + for lines in p.stdout.readlines(): + if isinstance(lines, bytes): + lines = lines.decode('utf-8') + if re.search('ebook-convert.*\(calibre', lines): + versions = lines + except Exception: + versions = _(u'Excecution permissions missing') + return {'Calibre converter' : versions} + + +def versioncheck(): + if ub.config.config_ebookconverter == 1: + return versionKindle() + elif ub.config.config_ebookconverter == 2: + return versionCalibre() + else: + return {'ebook_converter':''} + diff --git a/cps/db.py b/cps/db.py index f337ea4c..54e42d25 100755 --- a/cps/db.py +++ b/cps/db.py @@ -27,6 +27,14 @@ def title_sort(title): return title.strip() +def lcase(s): + return s.lower() + + +def ucase(s): + return s.upper() + + Base = declarative_base() books_authors_link = Table('books_authors_link', Base.metadata, @@ -59,6 +67,7 @@ books_publishers_link = Table('books_publishers_link', Base.metadata, Column('publisher', Integer, ForeignKey('publishers.id'), primary_key=True) ) + class Identifiers(Base): __tablename__ = 'identifiers' @@ -103,6 +112,8 @@ class Identifiers(Base): return u"https://books.google.com/books?id={0}".format(self.val) elif self.type == "kobo": return u"https://www.kobo.com/ebook/{0}".format(self.val) + elif self.type == "url": + return u"{0}".format(self.val) else: return u"" @@ -192,6 +203,7 @@ class Languages(Base): def __repr__(self): return u"".format(self.lang_code) + class Publishers(Base): __tablename__ = 'publishers' @@ -199,7 +211,7 @@ class Publishers(Base): name = Column(String) sort = Column(String) - def __init__(self, name,sort): + def __init__(self, name, sort): self.name = name self.sort = sort @@ -207,7 +219,6 @@ class Publishers(Base): return u"".format(self.name, self.sort) - class Data(Base): __tablename__ = 'data' @@ -245,7 +256,7 @@ class Books(Base): uuid = Column(String) authors = relationship('Authors', secondary=books_authors_link, backref='books') - tags = relationship('Tags', secondary=books_tags_link, backref='books') + tags = relationship('Tags', secondary=books_tags_link, backref='books',order_by="Tags.name") comments = relationship('Comments', backref='books') data = relationship('Data', backref='books') series = relationship('Series', secondary=books_series_link, backref='books') @@ -255,7 +266,7 @@ class Books(Base): identifiers = relationship('Identifiers', backref='books') def __init__(self, title, sort, author_sort, timestamp, pubdate, series_index, last_modified, path, has_cover, - authors, tags, languages = None): + authors, tags, languages=None): self.title = title self.sort = sort self.author_sort = author_sort @@ -271,6 +282,9 @@ class Books(Base): self.timestamp, self.pubdate, self.series_index, self.last_modified, self.path, self.has_cover) + @property + def atom_timestamp(self): + return (self.timestamp or '').replace(' ', 'T') class Custom_Columns(Base): __tablename__ = 'custom_columns' @@ -295,7 +309,7 @@ def setup_db(): global session global cc_classes - if config.config_calibre_dir is None or config.config_calibre_dir == u'': + if config.config_calibre_dir is None or config.config_calibre_dir == u'': content = ub.session.query(ub.Settings).first() content.config_calibre_dir = None content.db_configured = False @@ -304,8 +318,10 @@ def setup_db(): return False dbpath = os.path.join(config.config_calibre_dir, "metadata.db") - engine = create_engine('sqlite:///'+ dbpath, echo=False, isolation_level="SERIALIZABLE") try: + if not os.path.exists(dbpath): + raise + engine = create_engine('sqlite:///' + dbpath, echo=False, isolation_level="SERIALIZABLE", connect_args={'check_same_thread': False}) conn = engine.connect() except Exception: content = ub.session.query(ub.Settings).first() @@ -319,6 +335,8 @@ def setup_db(): ub.session.commit() config.loadSettings() conn.connection.create_function('title_sort', 1, title_sort) + conn.connection.create_function('lower', 1, lcase) + conn.connection.create_function('upper', 1, ucase) if not cc_classes: cc = conn.execute("SELECT id, datatype FROM custom_columns") @@ -363,8 +381,9 @@ def setup_db(): secondary=books_custom_column_links[cc_id[0]], backref='books')) - # Base.metadata.create_all(engine) - Session = sessionmaker() - Session.configure(bind=engine) + + Session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) session = Session() return True diff --git a/cps/epub.py b/cps/epub.py index dd9ad28b..10665576 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -12,7 +12,7 @@ def extractCover(zipFile, coverFile, coverpath, tmp_file_name): if coverFile is None: return None else: - zipCoverPath = os.path.join(coverpath , coverFile).replace('\\','/') + zipCoverPath = os.path.join(coverpath, coverFile).replace('\\', '/') cf = zipFile.read(zipCoverPath) prefix = os.path.splitext(tmp_file_name)[0] tmp_cover_name = prefix + '.' + os.path.basename(zipCoverPath) @@ -93,7 +93,7 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='"+meta_cover[0]+"']/@href", namespaces=ns) if len(coversection) > 0: filetype = coversection[0].rsplit('.', 1)[-1] - if filetype == "xhtml" or filetype == "html": #if cover is (x)html format + if filetype == "xhtml" or filetype == "html": # if cover is (x)html format markup = epubZip.read(os.path.join(coverpath, coversection[0])) markupTree = etree.fromstring(markup) # no matter xhtml or html with no namespace diff --git a/cps/fb2.py b/cps/fb2.py index 65d44848..87295ab8 100644 --- a/cps/fb2.py +++ b/cps/fb2.py @@ -3,10 +3,7 @@ from lxml import etree import uploader -#try: -# from io import StringIO -#except ImportError: -# import StringIO + def get_fb2_info(tmp_file_path, original_file_extension): @@ -23,41 +20,43 @@ def get_fb2_info(tmp_file_path, original_file_extension): def get_author(element): last_name = element.xpath('fb:last-name/text()', namespaces=ns) if len(last_name): - last_name = last_name[0] + last_name = last_name[0].encode('utf-8') else: last_name = u'' middle_name = element.xpath('fb:middle-name/text()', namespaces=ns) if len(middle_name): - middle_name = middle_name[0] + middle_name = middle_name[0].encode('utf-8') else: middle_name = u'' first_name = element.xpath('fb:first-name/text()', namespaces=ns) if len(first_name): - first_name = first_name[0] + first_name = first_name[0].encode('utf-8') else: first_name = u'' - return first_name + ' ' + middle_name + ' ' + last_name + return (first_name.decode('utf-8') + u' ' + + middle_name.decode('utf-8') + u' ' + + last_name.decode('utf-8')).encode('utf-8') author = str(", ".join(map(get_author, authors))) title = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:book-title/text()', namespaces=ns) if len(title): - title = str(title[0]) + title = str(title[0].encode('utf-8')) else: title = u'' description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns) if len(description): - description = str(description[0]) + description = str(description[0].encode('utf-8')) else: description = u'' return uploader.BookMeta( file_path=tmp_file_path, extension=original_file_extension, - title=title.encode('utf-8').decode('utf-8'), - author=author.encode('utf-8').decode('utf-8'), + title=title.decode('utf-8'), + author=author.decode('utf-8'), cover=None, - description=description, + description=description.decode('utf-8'), tags="", series="", series_id="", diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py index 8909db15..d8df9587 100644 --- a/cps/gdriveutils.py +++ b/cps/gdriveutils.py @@ -1,12 +1,17 @@ try: from pydrive.auth import GoogleAuth from pydrive.drive import GoogleDrive + from pydrive.auth import RefreshError from apiclient import errors + gdrive_support = True except ImportError: - pass -import os + gdrive_support = False +import os 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 @@ -15,9 +20,58 @@ from sqlalchemy.orm import * import web +class Singleton: + """ + A non-thread-safe helper class to ease implementing singletons. + This should be used as a decorator -- not a metaclass -- to the + class that should be a singleton. + + The decorated class can define one `__init__` function that + takes only the `self` argument. Also, the decorated class cannot be + inherited from. Other than that, there are no restrictions that apply + to the decorated class. + + To get the singleton instance, use the `Instance` method. Trying + to use `__call__` will result in a `TypeError` being raised. + + """ + + def __init__(self, decorated): + self._decorated = decorated + + def Instance(self): + """ + Returns the singleton instance. Upon its first call, it creates a + new instance of the decorated class and calls its `__init__` method. + On all subsequent calls, the already created instance is returned. + + """ + try: + return self._instance + except AttributeError: + self._instance = self._decorated() + return self._instance + + def __call__(self): + raise TypeError('Singletons must be accessed through `Instance()`.') + + def __instancecheck__(self, inst): + return isinstance(inst, self._decorated) + + +@Singleton +class Gauth: + def __init__(self): + self.auth = GoogleAuth(settings_file=os.path.join(config.get_main_dir,'settings.yaml')) + -dbpath = os.path.join(os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + os.sep + ".." + os.sep), "gdrive.db") -engine = create_engine('sqlite:///{0}'.format(dbpath), echo=False) +@Singleton +class Gdrive: + def __init__(self): + self.drive = getDrive(gauth=Gauth.Instance().auth) + + +engine = create_engine('sqlite:///{0}'.format(cli.gdpath), echo=False) Base = declarative_base() # Open session for database connection @@ -64,24 +118,28 @@ def migrate(): session.execute('ALTER TABLE gdrive_ids2 RENAME to gdrive_ids') break -if not os.path.exists(dbpath): +if not os.path.exists(cli.gdpath): try: Base.metadata.create_all(engine) except Exception: raise - migrate() def getDrive(drive=None, gauth=None): if not drive: if not gauth: - gauth = GoogleAuth(settings_file='settings.yaml') + gauth = GoogleAuth(settings_file=os.path.join(config.get_main_dir,'settings.yaml')) # Try to load saved client credentials - gauth.LoadCredentialsFile("gdrive_credentials") + gauth.LoadCredentialsFile(os.path.join(config.get_main_dir,'gdrive_credentials')) if gauth.access_token_expired: # Refresh them if expired - gauth.Refresh() + try: + gauth.Refresh() + except RefreshError as e: + web.app.logger.error("Google Drive error: " + e.message) + except Exception as e: + web.app.logger.exception(e) else: # Initialize the saved creds gauth.Authorize() @@ -91,41 +149,55 @@ def getDrive(drive=None, gauth=None): drive.auth.Refresh() return drive +def listRootFolders(drive=None): + drive = getDrive(drive) + folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false" + fileList = drive.ListFile({'q': folder}).GetList() + return fileList + def getEbooksFolder(drive=None): - drive = getDrive(drive) - ebooksFolder = "title = '%s' and 'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false" % config.config_google_drive_folder + return getFolderInFolder('root',config.config_google_drive_folder,drive) - fileList = drive.ListFile({'q': ebooksFolder}).GetList() - return fileList[0] +def getFolderInFolder(parentId, folderName,drive=None): + 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 + fileList = drive.ListFile({'q': folder}).GetList() + if fileList.__len__() == 0: + return None + else: + return fileList[0] +# Search for id of root folder in gdrive database, if not found request from gdrive and store in internal database def getEbooksFolderId(drive=None): storedPathName = session.query(GdriveId).filter(GdriveId.path == '/').first() if storedPathName: return storedPathName.gdrive_id else: gDriveId = GdriveId() - gDriveId.gdrive_id = getEbooksFolder(drive)['id'] + try: + gDriveId.gdrive_id = getEbooksFolder(drive)['id'] + except Exception: + web.app.logger.error('Error gDrive, root ID not found') gDriveId.path = '/' session.merge(gDriveId) session.commit() return -def getFolderInFolder(parentId, folderName, drive=None): - drive = getDrive(drive) - folder = "title = '%s' and '%s' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false" % (folderName.replace("'", "\\'"), parentId) - fileList = drive.ListFile({'q': folder}).GetList() - return fileList[0] - - -def getFile(pathId, fileName, drive=None): - drive = getDrive(drive) +def getFile(pathId, fileName, drive): + # drive = getDrive(Gdrive.Instance().drive) metaDataFile = "'%s' in parents and trashed = false and title = '%s'" % (pathId, fileName.replace("'", "\\'")) fileList = drive.ListFile({'q': metaDataFile}).GetList() - return fileList[0] + if fileList.__len__() == 0: + return None + else: + return fileList[0] def getFolderId(path, drive=None): @@ -146,12 +218,17 @@ def getFolderId(path, drive=None): if storedPathName: currentFolderId = storedPathName.gdrive_id else: - currentFolderId = getFolderInFolder(currentFolderId, x, drive)['id'] - gDriveId = GdriveId() - gDriveId.gdrive_id = currentFolderId - gDriveId.path = currentPath - session.merge(gDriveId) - dbChange = True + currentFolder = getFolderInFolder(currentFolderId, x, drive) + if currentFolder: + gDriveId = GdriveId() + gDriveId.gdrive_id = currentFolder['id'] + gDriveId.path = currentPath + session.merge(gDriveId) + dbChange = True + currentFolderId = currentFolder['id'] + else: + currentFolderId= None + break if dbChange: session.commit() else: @@ -159,15 +236,17 @@ def getFolderId(path, drive=None): return currentFolderId -def getFileFromEbooksFolder(drive, path, fileName): - drive = getDrive(drive) +def getFileFromEbooksFolder(path, fileName): + drive = getDrive(Gdrive.Instance().drive) if path: # sqlCheckPath=path if path[-1] =='/' else path + '/' folderId = getFolderId(path, drive) else: folderId = getEbooksFolderId(drive) - - return getFile(folderId, fileName, drive) + if folderId: + return getFile(folderId, fileName, drive) + else: + return None def copyDriveFileRemote(drive, origin_file_id, copy_title): @@ -182,22 +261,34 @@ def copyDriveFileRemote(drive, origin_file_id, copy_title): return None -def downloadFile(drive, path, filename, output): - drive = getDrive(drive) - f = getFileFromEbooksFolder(drive, path, filename) +# Download metadata.db from gdrive +def downloadFile(path, filename, output): + f = getFileFromEbooksFolder(path, filename) f.GetContentFile(output) -def backupCalibreDbAndOptionalDownload(drive, f=None): - drive = getDrive(drive) - metaDataFile = "'%s' in parents and title = 'metadata.db' and trashed = false" % getEbooksFolderId() - - fileList = drive.ListFile({'q': metaDataFile}).GetList() +def moveGdriveFolderRemote(origin_file, target_folder): + drive = getDrive(Gdrive.Instance().drive) + previous_parents = ",".join([parent["id"] for parent in origin_file.get('parents')]) + gFileTargetDir = getFileFromEbooksFolder(None, target_folder) + if not gFileTargetDir: + # Folder is not exisiting, create, and move folder + gFileTargetDir = drive.CreateFile( + {'title': target_folder, 'parents': [{"kind": "drive#fileLink", 'id': getEbooksFolderId()}], + "mimeType": "application/vnd.google-apps.folder"}) + gFileTargetDir.Upload() + # Move the file to the new folder + drive.auth.service.files().update(fileId=origin_file['id'], + 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() - databaseFile = fileList[0] - - if f: - databaseFile.GetContentFile(f) def copyToDrive(drive, uploadFile, createRoot, replaceFiles, @@ -231,8 +322,8 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles, driveFile.Upload() -def uploadFileToEbooksFolder(drive, destFile, f): - drive = getDrive(drive) +def uploadFileToEbooksFolder(destFile, f): + drive = getDrive(Gdrive.Instance().drive) parent = getEbooksFolder(drive) splitDir = destFile.split('/') for i, x in enumerate(splitDir): @@ -256,7 +347,7 @@ def uploadFileToEbooksFolder(drive, destFile, f): def watchChange(drive, channel_id, channel_type, channel_address, channel_token=None, expiration=None): - drive = getDrive(drive) + # drive = getDrive(drive) # Watch for all changes to a user's Drive. # Args: # service: Drive API service instance. @@ -299,7 +390,7 @@ def watchFile(drive, file_id, channel_id, channel_type, channel_address, Raises: apiclient.errors.HttpError: if http request to create channel fails. """ - drive = getDrive(drive) + # drive = getDrive(drive) body = { 'id': channel_id, @@ -322,7 +413,7 @@ def stopChannel(drive, channel_id, resource_id): Raises: apiclient.errors.HttpError: if http request to create channel fails. """ - drive = getDrive(drive) + # drive = getDrive(drive) # service=drive.auth.service body = { 'id': channel_id, @@ -332,7 +423,7 @@ def stopChannel(drive, channel_id, resource_id): def getChangeById (drive, change_id): - drive = getDrive(drive) + # drive = getDrive(drive) # Print a single Change resource information. # # Args: @@ -341,6 +432,73 @@ def getChangeById (drive, change_id): try: change = drive.auth.service.changes().get(changeId=change_id).execute() return change - except (errors.HttpError, error): - web.app.logger.exception(error) + except (errors.HttpError) as error: + web.app.logger.info(error.message) + return None + +# Deletes the local hashes database to force search for new folder names +def deleteDatabaseOnChange(): + session.query(GdriveId).delete() + session.commit() + +def updateGdriveCalibreFromLocal(): + copyToDrive(Gdrive.Instance().drive, config.config_calibre_dir, False, True) + for x in os.listdir(config.config_calibre_dir): + if os.path.isdir(os.path.join(config.config_calibre_dir, x)): + shutil.rmtree(os.path.join(config.config_calibre_dir, x)) + +# update gdrive.db on edit of books title +def updateDatabaseOnEdit(ID,newPath): + storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first() + if storedPathName: + storedPathName.path = newPath + session.commit() + +# Deletes the hashes in database of deleted book +def deleteDatabaseEntry(ID): + session.query(GdriveId).filter(GdriveId.gdrive_id == ID).delete() + session.commit() + +# Gets cover file from gdrive +def get_cover_via_gdrive(cover_path): + df = getFileFromEbooksFolder(cover_path, 'cover.jpg') + if df: + if not session.query(PermissionAdded).filter(PermissionAdded.gdrive_id == df['id']).first(): + df.GetPermissions() + df.InsertPermission({ + 'type': 'anyone', + 'value': 'anyone', + 'role': 'reader', + 'withLink': True}) + permissionAdded = PermissionAdded() + permissionAdded.gdrive_id = df['id'] + session.add(permissionAdded) + session.commit() + return df.metadata.get('webContentLink') + else: return None + +# Creates chunks for downloading big files +def partial(total_byte_len, part_size_limit): + s = [] + for p in range(0, total_byte_len, part_size_limit): + last = min(total_byte_len - 1, p + part_size_limit - 1) + s.append([p, last]) + return s + +# downloads files in chunks from gdrive +def do_gdrive_download(df, headers): + total_size = int(df.metadata.get('fileSize')) + download_url = df.metadata.get('downloadUrl') + s = partial(total_size, 1024 * 1024) # I'm downloading BIG files, so 100M chunk size is fine for me + + def stream(): + for byte in s: + headers = {"Range": 'bytes=%s-%s' % (byte[0], byte[1])} + resp, content = df.auth.Get_Http_Object().request(download_url, headers=headers) + if resp.status == 206: + yield content + else: + web.app.logger.info('An error occurred: %s' % resp) + return + return Response(stream_with_context(stream()), headers=headers) diff --git a/cps/helper.py b/cps/helper.py index 0b2e50ef..a5021b6b 100755 --- a/cps/helper.py +++ b/cps/helper.py @@ -5,258 +5,144 @@ import db import ub from flask import current_app as app import logging -import smtplib from tempfile import gettempdir -import socket import sys import os -import traceback import re import unicodedata +from io import BytesIO +import worker +import time -try: - from StringIO import StringIO - from email.MIMEBase import MIMEBase - from email.MIMEMultipart import MIMEMultipart - from email.MIMEText import MIMEText -except ImportError as e: - from io import StringIO - from email.mime.base import MIMEBase - from email.mime.multipart import MIMEMultipart - from email.mime.text import MIMEText - -from email import encoders -from email.generator import Generator -from email.utils import formatdate -from email.utils import make_msgid +from flask import send_from_directory, make_response, redirect, abort from flask_babel import gettext as _ -import subprocess import threading import shutil import requests import zipfile -from tornado.ioloop import IOLoop try: import gdriveutils as gd except ImportError: pass import web +import server +import random +import subprocess try: import unidecode use_unidecode = True -except Exception as e: +except ImportError: use_unidecode = False # Global variables -global_task = None updater_thread = None +global_WorkerThread = worker.WorkerThread() +global_WorkerThread.start() -RET_SUCCESS = 1 -RET_FAIL = 0 def update_download(book_id, user_id): check = ub.session.query(ub.Downloads).filter(ub.Downloads.user_id == user_id).filter(ub.Downloads.book_id == book_id).first() - if not check: new_download = ub.Downloads(user_id=user_id, book_id=book_id) ub.session.add(new_download) ub.session.commit() - -def make_mobi(book_id, calibrepath): - error_message = None - vendorpath = os.path.join(os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + - os.sep + "../vendor" + os.sep)) - if sys.platform == "win32": - kindlegen = (os.path.join(vendorpath, u"kindlegen.exe")).encode(sys.getfilesystemencoding()) - else: - kindlegen = (os.path.join(vendorpath, u"kindlegen")).encode(sys.getfilesystemencoding()) - if not os.path.exists(kindlegen): - error_message = _(u"kindlegen binary %(kindlepath)s not found", kindlepath=kindlegen) - app.logger.error("make_mobi: " + error_message) - return error_message, RET_FAIL +# Convert existing book entry to new format +def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, user_id, kindle_mail=None): 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).filter(db.Data.format == 'EPUB').first() + data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == old_book_format).first() if not data: - error_message = _(u"epub format not found for book id: %(book)d", book=book_id) - app.logger.error("make_mobi: " + error_message) - return error_message, RET_FAIL - + error_message = _(u"%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id) + app.logger.error("convert_book_format: " + error_message) + return error_message + if ub.config.config_use_google_drive: + df = gd.getFileFromEbooksFolder(book.path, data.name + "." + old_book_format.lower()) + if df: + datafile = os.path.join(calibrepath, book.path, data.name + u"." + old_book_format.lower()) + if not os.path.exists(os.path.join(calibrepath, book.path)): + os.makedirs(os.path.join(calibrepath, book.path)) + df.GetContentFile(datafile) + else: + error_message = _(u"%(format)s not found on Google Drive: %(fn)s", + format=old_book_format, fn=data.name + "." + old_book_format.lower()) + return error_message file_path = os.path.join(calibrepath, book.path, data.name) - if os.path.exists(file_path + u".epub"): - try: - p = subprocess.Popen((kindlegen + " \"" + file_path + u".epub\"").encode(sys.getfilesystemencoding()), - stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) - except Exception: - error_message = _(u"kindlegen failed, no excecution permissions") - app.logger.error("make_mobi: " + error_message) - return error_message, RET_FAIL - # Poll process for new output until finished - while True: - nextline = p.stdout.readline() - if nextline == '' and p.poll() is not None: - break - if nextline != "\r\n": - # Format of error message (kindlegen translates its output texts): - # Error(prcgen):E23006: Language not recognized in metadata.The dc:Language field is mandatory.Aborting. - conv_error=re.search(".*\(.*\):(E\d+):\s(.*)",nextline) - # If error occoures, log in every case - if conv_error: - error_message = _(u"Kindlegen failed with Error %(error)s. Message: %(message)s", - error=conv_error.group(1), message=conv_error.group(2).decode('utf-8')) - app.logger.info("make_mobi: " + error_message) - app.logger.info(nextline.strip('\r\n')) - app.logger.debug(nextline.strip('\r\n')) - - check = p.returncode - if not check or check < 2: - book.data.append(db.Data( - name=book.data[0].name, - book_format="MOBI", - book=book.id, - uncompressed_size=os.path.getsize(file_path + ".mobi") - )) - db.session.commit() - return file_path + ".mobi", RET_SUCCESS + if os.path.exists(file_path + "." + old_book_format.lower()): + # read settings and append converter task to queue + if kindle_mail: + settings = ub.get_mail_settings() + text = _(u"Convert: %s" % book.title) else: - app.logger.info("make_mobi: kindlegen failed with error while converting book") - if not error_message: - error_message='kindlegen failed, no excecution permissions' - return error_message, RET_FAIL + text = _(u"Convert to %(format)s: %(book)s", format=new_book_format, book=book.title) + settings['old_book_format'] = u'EPUB' + settings['new_book_format'] = u'MOBI' + global_WorkerThread.add_convert(file_path, book.id, user_id, text, settings, kindle_mail) + return None else: - error_message = "make_mobi: epub not found: %s.epub" % file_path - return error_message, RET_FAIL - - -class StderrLogger(object): - - buffer = '' - - def __init__(self): - self.logger = logging.getLogger('cps.web') - - def write(self, message): - if message == '\n': - self.logger.debug(self.buffer) - self.buffer = '' - else: - self.buffer += message - - -def send_raw_email(kindle_mail, msg): - settings = ub.get_mail_settings() - - msg['From'] = settings["mail_from"] - msg['To'] = kindle_mail - - use_ssl = int(settings.get('mail_use_ssl', 0)) - - # convert MIME message to string - fp = StringIO() - gen = Generator(fp, mangle_from_=False) - gen.flatten(msg) - msg = fp.getvalue() - - # send email - try: - timeout = 600 # set timeout to 5mins - - org_stderr = smtplib.stderr - smtplib.stderr = StderrLogger() - - if use_ssl == 2: - mailserver = smtplib.SMTP_SSL(settings["mail_server"], settings["mail_port"], timeout) - else: - mailserver = smtplib.SMTP(settings["mail_server"], settings["mail_port"], timeout) - mailserver.set_debuglevel(1) - - if use_ssl == 1: - mailserver.starttls() - - if settings["mail_password"]: - mailserver.login(str(settings["mail_login"]), str(settings["mail_password"])) - mailserver.sendmail(settings["mail_from"], kindle_mail, msg) - mailserver.quit() - - smtplib.stderr = org_stderr - - except (socket.error, smtplib.SMTPRecipientsRefused, smtplib.SMTPException) as e: - app.logger.error(traceback.print_exc()) - return _("Failed to send mail: %s" % str(e)) - - return None - - -def send_test_mail(kindle_mail): - msg = MIMEMultipart() - msg['Subject'] = _(u'Calibre-web test email') - text = _(u'This email has been sent via calibre web.') - msg.attach(MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8')) - return send_raw_email(kindle_mail, msg) - - -def send_mail(book_id, kindle_mail, calibrepath): + error_message = _(u"%(format)s not found: %(fn)s", + format=old_book_format, fn=data.name + "." + old_book_format.lower()) + return error_message + + +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")) + return + + +# Send registration email or password reset email, depending on parameter resend (False means welcome email) +def send_registration_mail(e_mail, user_name, default_password, resend=False): + text = "Hello %s!\r\n" % user_name + if not resend: + text += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n" + text += "Please log in to your account using the following informations:\r\n" + text += "User name: %s\n" % user_name + text += "Password: %s\r\n" % default_password + text += "Don't forget to change your password after first login.\r\n" + 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: %s" % user_name),text) + return + + +# 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): """Send email with attachments""" - # create MIME message - msg = MIMEMultipart() - msg['Subject'] = _(u'Send to Kindle') - msg['Message-Id'] = make_msgid('calibre-web') - msg['Date'] = formatdate(localtime=True) - text = _(u'This email has been sent via calibre web.') - msg.attach(MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8')) - 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) + data = db.session.query(db.Data).filter(db.Data.book == book.id).all() formats = {} - for entry in data: if entry.format == "MOBI": - formats["mobi"] = os.path.join(calibrepath, book.path, entry.name + ".mobi") + formats["mobi"] = entry.name + ".mobi" if entry.format == "EPUB": - formats["epub"] = os.path.join(calibrepath, book.path, entry.name + ".epub") + formats["epub"] = entry.name + ".epub" if entry.format == "PDF": - formats["pdf"] = os.path.join(calibrepath, book.path, entry.name + ".pdf") + formats["pdf"] = entry.name + ".pdf" if len(formats) == 0: - return _("Could not find any formats suitable for sending by email") + return _(u"Could not find any formats suitable for sending by e-mail") if 'mobi' in formats: - msg.attach(get_attachment(formats['mobi'])) + result = formats['mobi'] elif 'epub' in formats: - data, resultCode = make_mobi(book.id, calibrepath) - if resultCode == RET_SUCCESS: - msg.attach(get_attachment(data)) - else: - app.logger.error = (data) - return data #_("Could not convert epub to mobi") + # 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: - msg.attach(get_attachment(formats['pdf'])) + result = formats['pdf'] # worker.get_attachment() else: - return _("Could not find any formats suitable for sending by email") - - return send_raw_email(kindle_mail, msg) - - -def get_attachment(file_path): - """Get file as MIMEBase message""" - - try: - file_ = open(file_path, 'rb') - attachment = MIMEBase('application', 'octet-stream') - attachment.set_payload(file_.read()) - file_.close() - encoders.encode_base64(attachment) - - attachment.add_header('Content-Disposition', 'attachment', - filename=os.path.basename(file_path)) - return attachment - except IOError: - traceback.print_exc() - app.logger.error = (u'The requested file could not be read. Maybe wrong permissions?') - return None + 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: %s" % book.title)) + else: + return _(u"The requested file could not be read. Maybe wrong permissions?") def get_valid_filename(value, replace_whitespace=True): @@ -268,68 +154,104 @@ def get_valid_filename(value, replace_whitespace=True): value = value[:-1]+u'_' value = value.replace("/", "_").replace(":", "_").strip('\0') if use_unidecode: - value=(unidecode.unidecode(value)).strip() + value = (unidecode.unidecode(value)).strip() else: - value=value.replace(u'§',u'SS') - value=value.replace(u'ß',u'ss') + value = value.replace(u'§', u'SS') + value = value.replace(u'ß', u'ss') value = unicodedata.normalize('NFKD', value) re_slugify = re.compile('[\W\s-]', re.UNICODE) - if isinstance(value, str): #Python3 str, Python2 unicode + if isinstance(value, str): # Python3 str, Python2 unicode value = re_slugify.sub('', value).strip() else: value = unicode(re_slugify.sub('', value).strip()) if replace_whitespace: - #*+:\"/<>? are replaced by _ - value = re.sub('[\*\+:\\\"/<>\?]+', u'_', value, flags=re.U) - + # *+:\"/<>? are replaced by _ + value = re.sub(r'[\*\+:\\\"/<>\?]+', u'_', value, flags=re.U) + # pipe has to be replaced with comma + value = re.sub(r'[\|]+', u',', value, flags=re.U) value = value[:128] if not value: raise ValueError("Filename cannot be empty") - return value + def get_sorted_author(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] - else: - value2 = value[-1] + ", " + " ".join(value[:-1]) + 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] + else: + value2 = value[-1] + ", " + " ".join(value[:-1]) + except Exception: + web.app.logger.error("Sorting author " + str(value) + "failed") + value2 = value return value2 -def delete_book(book, calibrepath): - path = os.path.join(calibrepath, book.path)#.replace('/',os.path.sep)).replace('\\',os.path.sep) - shutil.rmtree(path, ignore_errors=True) -def delete_book_gdrive(book): - pass +# Deletes a book fro the local filestorage, returns True if deleting is successfull, otherwise false +def delete_book_file(book, calibrepath, book_format=None): + # check that path is 2 elements deep, check that target path has no subfolders + if book.path.count('/') == 1: + path = os.path.join(calibrepath, book.path) + if book_format: + for file in os.listdir(path): + if file.upper().endswith("."+book_format): + os.remove(os.path.join(path, file)) + else: + if os.path.isdir(path): + if len(next(os.walk(path))[1]): + web.app.logger.error( + "Deleting book " + str(book.id) + " failed, path has subfolders: " + book.path) + return False + shutil.rmtree(path, ignore_errors=True) + return True + else: + web.app.logger.error("Deleting book " + str(book.id) + " failed, book path not valid: " + book.path) + return False -def update_dir_stucture(book_id, calibrepath): - db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) - book = db.session.query(db.Books).filter(db.Books.id == book_id).first() - path = os.path.join(calibrepath, book.path) - authordir = book.path.split('/')[0] - new_authordir = get_valid_filename(book.authors[0].name) - titledir = book.path.split('/')[1] - new_titledir = get_valid_filename(book.title) + " (" + str(book_id) + ")" +def update_dir_structure_file(book_id, calibrepath): + localbook = db.session.query(db.Books).filter(db.Books.id == book_id).first() + path = os.path.join(calibrepath, localbook.path) - if titledir != new_titledir: - new_title_path = os.path.join(os.path.dirname(path), new_titledir) - os.rename(path, new_title_path) - path = new_title_path - book.path = book.path.split('/')[0] + '/' + new_titledir + authordir = localbook.path.split('/')[0] + new_authordir = get_valid_filename(localbook.authors[0].name) + + titledir = localbook.path.split('/')[1] + new_titledir = get_valid_filename(localbook.title) + " (" + str(book_id) + ")" + if titledir != new_titledir: + try: + new_title_path = os.path.join(os.path.dirname(path), new_titledir) + if not os.path.exists(new_title_path): + os.renames(path, new_title_path) + else: + 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)) + 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) + web.app.logger.error(ex, exc_info=True) + return _('Rename title from: "%s" to "%s" failed with error: %s' % (path, new_title_path, str(ex))) if authordir != new_authordir: - new_author_path = os.path.join(os.path.join(calibrepath, new_authordir), os.path.basename(path)) - os.renames(path, new_author_path) - book.path = new_authordir + '/' + book.path.split('/')[1] - db.session.commit() + try: + new_author_path = os.path.join(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) + web.app.logger.error(ex, exc_info=True) + return _('Rename author from: "%s" to "%s" failed with error: %s' % (path, new_title_path, str(ex))) + return False def update_dir_structure_gdrive(book_id): - db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) + error = False book = db.session.query(db.Books).filter(db.Books.id == book_id).first() authordir = book.path.split('/')[0] @@ -338,60 +260,153 @@ def update_dir_structure_gdrive(book_id): new_titledir = get_valid_filename(book.title) + " (" + str(book_id) + ")" if titledir != new_titledir: - print (titledir) - gFile=gd.getFileFromEbooksFolder(web.Gdrive.Instance().drive,os.path.dirname(book.path),titledir) - gFile['title']= new_titledir - gFile.Upload() - book.path = book.path.split('/')[0] + '/' + new_titledir + # print (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 + gd.updateDatabaseOnEdit(gFile['id'], book.path) # only child folder affected + else: + error = _(u'File %s not found on Google Drive' % book.path) # file not found if authordir != new_authordir: - gFile=gd.getFileFromEbooksFolder(web.Gdrive.Instance().drive,None,authordir) - gFile['title'] = new_authordir - gFile.Upload() - book.path = new_authordir + '/' + book.path.split('/')[1] + gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir) + if gFile: + gd.moveGdriveFolderRemote(gFile,new_authordir) + book.path = new_authordir + '/' + book.path.split('/')[1] + gd.updateDatabaseOnEdit(gFile['id'], book.path) + else: + error = _(u'File %s not found on Google Drive' % authordir) # file not found + return error + + +def delete_book_gdrive(book, book_format): + error= False + if book_format: + name = '' + for entry in book.data: + if entry.format.upper() == book_format: + name = entry.name + '.' + book_format + gFile = gd.getFileFromEbooksFolder(book.path, name) + else: + gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path),book.path.split('/')[1]) + if gFile: + gd.deleteDatabaseEntry(gFile['id']) + gFile.Trash() + else: + error =_(u'Book path %s not found on Google Drive' % book.path) # file not found + return error + +def generate_random_password(): + s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?" + passlen = 8 + return "".join(random.sample(s,passlen )) + +################################## External interface + +def update_dir_stucture(book_id, calibrepath): + if ub.config.config_use_google_drive: + return update_dir_structure_gdrive(book_id) + else: + return update_dir_structure_file(book_id, calibrepath) + +def delete_book(book, calibrepath, book_format): + if ub.config.config_use_google_drive: + return delete_book_gdrive(book, 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: + path=gd.get_cover_via_gdrive(cover_path) + if path: + return redirect(path) + else: + 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.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) + if img.headers.get('content-type') != 'image/jpeg': + web.app.logger.error("Cover is no jpg file, can't save") + return False + + if ub.config.config_use_google_drive: + tmpDir = gettempdir() + 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)) + web.app.logger.info("Cover is saved on Google Drive") + return True + + f = open(os.path.join(ub.config.config_calibre_dir, book_path, "cover.jpg"), "wb") + f.write(img.content) + f.close() + 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() + df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format) + web.app.logger.debug(time.time() - startTime) + if df: + return gd.do_gdrive_download(df, 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)) + response.headers = headers + return response + +################################## - db.session.commit() class Updater(threading.Thread): def __init__(self): threading.Thread.__init__(self) - self.status=0 + self.status = 0 def run(self): - global global_task 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(StringIO(r.content)) + 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.update_source(os.path.join(tmp_dir, os.path.splitext(fname)[0]), ub.config.get_main_dir) self.status = 5 - global_task = 0 db.session.close() db.engine.dispose() ub.session.close() ub.engine.dispose() self.status = 6 - - if web.gevent_server: - web.gevent_server.stop() - else: - # stop tornado server - server = IOLoop.instance() - server.add_callback(server.stop) + server.Server.setRestartTyp(True) + server.Server.stopServer() self.status = 7 def get_update_status(self): return self.status @classmethod - def file_to_list(self, file): - return [x.strip() for x in open(file, 'r') if not x.startswith('#EXT')] + 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): @@ -400,8 +415,8 @@ class Updater(threading.Thread): @classmethod def reduce_dirs(self, delete_files, new_list): new_delete = [] - for file in delete_files: - parts = file.split(os.sep) + for filename in delete_files: + parts = filename.split(os.sep) sub = '' for part in parts: sub = os.path.join(sub, part) @@ -459,8 +474,8 @@ class Updater(threading.Thread): if change_permissions: try: os.chown(dst_file, permission.st_uid, permission.st_gid) - except Exception as e: - e = sys.exc_info() + 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: ' @@ -508,3 +523,27 @@ class Updater(threading.Thread): 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): + try: + if sys.version_info < (3, 0): + unrarLocation = unrarLocation.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(unrarLocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p.wait() + for lines in p.stdout.readlines(): + if isinstance(lines, bytes): + lines = lines.decode('utf-8') + value=re.search('UNRAR (.*) freeware', lines) + if value: + version = value.group(1) + except OSError as e: + error = True + web.app.logger.exception(e) + version =_(u'Error excecuting UnRar') + else: + version = _(u'Unrar binary file not found') + error=True + return (error, version) + diff --git a/cps/server.py b/cps/server.py new file mode 100644 index 00000000..37245b42 --- /dev/null +++ b/cps/server.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +from socket import error as SocketError +import sys +import os +try: + from gevent.pywsgi import WSGIServer + from gevent.pool import Pool + from gevent import __version__ as geventVersion + gevent_present = True +except ImportError: + from tornado.wsgi import WSGIContainer + from tornado.httpserver import HTTPServer + from tornado.ioloop import IOLoop + from tornado import version as tornadoVersion + gevent_present = False + +import web + + +class server: + + wsgiserver = None + restart= False + + def __init__(self): + pass + + 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()} + 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) + 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() + except Exception: + web.app.logger.info("Unknown error while starting gevent") + + def startServer(self): + if gevent_present: + web.app.logger.info('Starting Gevent 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) + + if self.restart == True: + web.app.logger.info("Performing restart of Calibre-Web") + web.helper.global_WorkerThread.stop() + if os.name == 'nt': + arguments = ["\"" + sys.executable + "\""] + for e in sys.argv: + arguments.append("\"" + e + "\"") + os.execv(sys.executable, arguments) + else: + os.execl(sys.executable, sys.executable, *sys.argv) + else: + web.app.logger.info("Performing shutdown of Calibre-Web") + web.helper.global_WorkerThread.stop() + sys.exit(0) + + def setRestartTyp(self,starttyp): + self.restart=starttyp + + def stopServer(self): + if gevent_present: + self.wsgiserver.close() + else: + self.wsgiserver.add_callback(self.wsgiserver.stop) + + @staticmethod + def getNameVersion(): + if gevent_present: + return {'Gevent':'v'+geventVersion} + else: + return {'Tornado':'v'+tornadoVersion} + + +# Start Instance of Server +Server=server() diff --git a/cps/static/css/caliBlur-style.css b/cps/static/css/caliBlur-style.css new file mode 100644 index 00000000..ddff69bc --- /dev/null +++ b/cps/static/css/caliBlur-style.css @@ -0,0 +1,2292 @@ +#have_read_form { + width: 1px; + height: 1px; + position: fixed; + top: 5px; + right: 185px; +} +#have_read_form input[type="checkbox" i] { + visibility: hidden; +} +#have_read_form input[type="checkbox" i]:hover:before { + cursor: pointer; + color: #fff; +} +#have_read_form input[type="checkbox" i]:before { + content: "\e106"; + position: fixed; + width: 60px; + height: 60px; + background: transparent; + top: 0; + right: 180px; + visibility: visible; + border-right: 2px solid rgba(0,0,0,.3); + border-left: 2px solid rgba(0,0,0,.3); + padding: 20px 39px 40px 19px; + color: hsla(0,0%,100%,.7); + z-index: 999; +} +#have_read_form input[type="checkbox" i]:checked:before { + content: "\e105"; + position: fixed; + width: 60px; + height: 60px; + background: transparent; + top: 0; + right: 180px; + visibility: visible; + color: var(--color-primary); + padding: 20px 39px 40px 19px; + z-index: 999; +} +.col-sm-10 #have_read_form input[type="checkbox" i]:before, +.col-sm-10 #have_read_form input[type="checkbox" i]:checked:before { + top: 60px; +} +#have_read_form input[type="checkbox" i]:checked:hover:before { + color: #fff; +} +#have_read_form span { + display: none; +} +#have_read_cb { + + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-size: 18px; + font-weight: 400; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.book { + width: 225px; + max-width: 225px; +} + +#infscr-loading img { + display: none; +} +#infscr-loading:before { + content: ''; + -webkit-box-sizing: border-box; + box-sizing: border-box; + border: 2px solid transparent; + border-radius: 50%; + border-top: 2px solid var(--color-secondary); + width: 24px; + height: 24px; + -webkit-animation: spin 0.5s linear infinite; + animation: spin 0.5s linear infinite; + display: block; + margin: 20px auto; + position: fixed; + top: calc(50% - 12px); + left: calc(50% - 12px); + z-index: 999999; +} +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + } +} +@keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +a { + color: hsla(0, 0%, 100%, .45); +} +a:hover { + transition: color 0.2s; + color: #fff; +} +body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a { + margin: 60px auto auto !important; + width: 240px; + height: 60px; + text-align: center; + color: var(--color-primary) !important; + line-height: 60px; + padding-top: 0; + font-size: 15px; + font-family: Open Sans Bold,Helvetica Neue,Helvetica,Arial,sans-serif; + font-stretch: normal; + font-style: normal; + font-variant-caps: normal; + font-variant-ligatures: normal; + font-variant-numeric: normal; + font-weight: 600; + letter-spacing: normal; + text-shadow: none; + text-size-adjust: 100%; + text-transform: none; + user-select: none; + word-spacing: 0; + writing-mode: horizontal-tb; + -webkit-box-direction: normal; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + padding-bottom: 0; + padding-left: 40px; + padding-right: 40px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + background: rgba(0,0,0,.45); +} +body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a:before { + background: url("https://raw.githubusercontent.com/leram84/layer.Cake/master/Resources/home-gray.png"); + background-size: cover; + background-repeat: no-repeat; + position: absolute; + display: block; + overflow: visible; + width: 25px; + height: 25px; + top: 16.5px; + left: 18px; + content: ''; +} +body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a:hover:after { + background: url("https://raw.githubusercontent.com/leram84/layer.Cake/master/Resources/home-white.png"); + background-size: cover; + background-repeat: no-repeat; + position: absolute; + display: block; + overflow: visible; + width: 25px; + height: 25px; + top: 16.5px; + left: 18px; + content: ''; +} +/* body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a:hover { + background: rgba(0,0,0,.45); + color: #fff !important; +} */ + :root { + --color-background: #474747; + --color-primary: #F9BE03; + --color-secondary: #CC7B19; + --color-secondary-hover: #E59029; +} +/* Root For Emby */ +/* :root { + --color-background: #333333; + --color-primary: #22aadc; + --color-secondary: #52b54b; + --color-secondary-hover: #71c26b; +} color: hsla(0,0%,100%,.45);*/ + +/* Blur Background */ +body { + background-image: url(https://raw.githubusercontent.com/leram84/layer.Cake/master/Resources/blur-noise.png), url("https://raw.githubusercontent.com/leram84/layer.Cake/master/Resources/blur-light.png") !important; + background-repeat: repeat, no-repeat !important; + background-attachment: fixed, fixed !important; + background-position: center center, center center !important; + background-size: auto, cover !important; + -webkit-background-size: auto, cover !important; + -moz-background-size: auto, cover !important; + -o-background-size: auto, cover !important; +} +.navbar-default { + background-color: rgba(0,0,0,.7) !important; + border: none; + position: fixed; + width: 100%; + height: 60px; + z-index: -9; +} +body { + background: url(https://raw.githubusercontent.com/leram84/layer.Cake/master/Resources/blur-noise.png), var(--color-background); + color: hsla(0, 0%, 100%, .45); + font-family: Open Sans Regular, Helvetica Neue, Helvetica, Arial, sans-serif; + font-weight: 600; + overflow: hidden; + margin: 0; +} +body > div.navbar.navbar-default.navbar-static-top > div > form > div { + width: 360px; +} +#btn-upload { + min-width: 1px; + min-height: 1px; + width: 60px; + height: 60px; + left: 0; + line-height: 1; + font-size: 1px; +} +#btn-upload:hover { + cursor: pointer !important; +} +#main-nav > li:nth-child(1) { + float: right; + +} +#form-upload { + position: relative; + float: right; + width: 60px; + margin: 0; + padding: 0; + height: 60px; + margin-right: 0; +} + +#form-upload .form-group .btn { + color: rgba(0,0,0,0); + background: transparent; + border: 0; + text-transform: uppercase; + font-weight: 600; + margin: 0; + padding: 0; + height: 60px; +} +#form-upload:hover .form-group .btn:after { + color: var(--color-secondary-hover); +} +#form-upload:hover .form-group .btn:before { + -webkit-box-shadow: inset 0 0 0 2px #fff; + box-shadow: inset 0 0 0 2px #fff; + background: var(--color-secondary-hover); + color: #fff; +} +#form-upload .form-group .btn:before { + content: "\e043"; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: 400; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + color: #eee; + font-size: 20px; + background: var(--color-secondary); + border-radius: 50%; + line-height: 35px; + width: 35px; + height: 35px; + position: fixed; + top: 12.5px; + right: 25px; + margin: 0; + padding: 0 2px 0 0; + z-index: -9; +} +#form-upload .form-group .btn:after { + content: "+"; + font-size: 14px; + color: var(--color-secondary); + background: transparent; + border-radius: 50%; + padding: 0; + z-index: 9; + position: fixed; + line-height: 12px; + width: 12px; + height: 12px; + top: 26px; + right: 38px; + /* + content: "+"; + font-size: 14px; + color: #fff; + background: var(--color-secondary); + border-radius: 50%; + padding: 0; + z-index: 9; + position: fixed; + line-height: 14px; + width: 14px; + height: 14px; + top: 10px; + right: 20px; + */ +} +.form-control { + background-image: none; + display: block; + width: 100%; + height: 38px; + padding: 6px 12px; + font-weight: 100; + font-family: "Open Sans Semibold", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px !important; + font-stretch: 100%; + font-style: normal; + font-variant-caps: normal; + font-variant-east-asian: normal; + font-variant-ligatures: normal; + font-variant-numeric: normal; + font-weight: 400; + letter-spacing: normal; + line-height: 22.2857px !important; + color: #eee; + vertical-align: middle; + background-color: hsla(0, 0%, 100%, .25); + border-radius: 3px; + border: 0 !important; + -webkit-box-shadow: none; + box-shadow: none; + -webkit-transition: background-color 0.2s; + transition: background-color 0.2s; +} +.form-control:focus { + border-color: transparent; + outline: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: #eee !important; + color: #555; +} +.form-control.tt-hint { + background: hsla(0, 0%, 100%, .25) !important; +} + +body > div.navbar.navbar-default.navbar-static-top > div > form > div > span > button { + background-color: hsla(0, 0%, 100%, .25); + color: #eee; + border: none; + +} + +body > div.navbar.navbar-default.navbar-static-top > div > form { + margin-left: -167px; + padding: 0; + margin-top: 15px; + margin-bottom: 15px; +} +body > div.navbar.navbar-default.navbar-static-top > div > form > .form-group > input { + border-radius: 4px 0 0 4px; +} +#main-nav { + margin-right: 15px; +} +body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul { + height: 60px; + +} +body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li { + height: 60px; + +} +body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li > a { + height: 60px; + padding: 20px 10px; + color: #999; +} +body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul:nth-child(1) {} +.author > a:hover { + color: #fff !important; +} +.row { + margin: 0; +} +body > div.container-fluid > div > div.col-sm-10 > div > form > h2 { + font-size: 15px; + color: var(--color-primary); + border-top: 1px solid rgba(0,0,0,.3); + padding-top: 60px; + margin-top: 50px; + margin-bottom: 30px; +} +/* clean up */ +.navigation .nav-head:nth-child(1n+2) { + border-top: none; + padding-top: 0; +} +.btn-default { + border-color: var(--color-secondary); + color: #fff; + background-color: var(--color-secondary); + -webkit-transition: background-color 0.1s; + transition: background-color 0.1s; +} +.btn-default:hover { + border-color: var(--color-secondary-hover); + color: #fff; + background-color: var(--color-secondary-hover); +} +body > div.container-fluid > div > div.col-sm-10 > div > form > a { + background-color: hsla(0,0%,100%,.25); + color: #fff; +} +body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover { + background-color: hsla(0,0%,100%,.3); + color: #fff; +} +.btn { + display: inline-block; + padding: 6px 16px; + font-family: Open Sans Bold,Helvetica Neue,Helvetica,Arial,sans-serif; + text-transform: uppercase; + border: 0; + margin-bottom: 0; + font-size: 14px; + font-weight: 400; + line-height: 1.71428571; + text-align: center; + vertical-align: middle; + cursor: pointer; + border-radius: 3px; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} +.btn-primary { + color: #fff; + background-color: hsla(0,0%,100%,.25); + border-color: hsla(0,0%,100%,.25); +} +.btn-primary:hover { + background-color: hsla(0,0%,100%,.3); + border-color: hsla(0,0%,100%,.3); +} +.btn-default.focus, .btn-default:focus { + border-color: var(--color-secondary-hover); + color: #fff; + background-color: var(--color-secondary-hover); +} +.btn-default.active, .btn-default:active, .open>.dropdown-toggle.btn-default { + border-color: var(--color-secondary-hover); + color: #fff; + background-color: var(--color-secondary-hover); +} + +.form-group > label { + font-weight: 100; + color: #fff; +} +.navbar > .container-fluid { + margin: 0; + padding: 0; + height: 60px; +} +.navigation .nav-head { + text-transform: uppercase; + margin: 0; + color: hsla(0,0%,100%,.3); + text-transform: uppercase; + font-size: 12px; + font-family: Open Sans Bold,Helvetica Neue,Helvetica,Arial,sans-serif; + line-height: 45px; + padding-left: 25px; +} +.navigation li a { + color: hsla(0,0%,100%,.7); + text-decoration: none; + padding: 4px 0; + font-size: 13px; + height: 30px; + line-height: 21px; + width: 100%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} +.navigation li { + min-height: 30px; + border-radius: 0 4px 4px 0; + max-width: 265px; + padding: 0 25px; + overflow: visible; +} +.navigation li:hover { + background-color: hsla(0,0%,100%,.08); + +} +.navigation li.nav-head:hover { + background-color: transparent; +} +.navigation li a:hover { + background: transparent; + color: #fff; +} +.navigation .create-shelf a { + + width: 150px; + color: #fff !important; + background-color: hsla(0, 0%, 100%, .25); + padding: 2px 7px; + font-size: 12px; + line-height: 1.2; + border-radius: 3px; + font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif; + border: 0; + -webkit-transition: background-color 0.1s; + transition: background-color 0.1s; + font-weight: 600; + white-space: nowrap; + margin: auto; + text-transform: uppercase; + max-height: 20px; +} +#scnd-nav .dropdown-menu > li > a:hover { + color: #fff; + background: transparent; +} +#scnd-nav .dropdown-menu { + left: 5px; +} +.navigation .create-shelf a:hover { + background-color: hsla(0, 0%, 100%, .3); + border-radius: 3px; +} +.navigation .create-shelf a:before { + content: "+"; + font-size: 120%; + padding-right: 10px; +} +.navigation .create-shelf { + width: auto; + margin: 30px auto; + min-height: auto; +} +.navigation .create-shelf:hover { + background: transparent; +} + +.container-fluid .book .meta .title { + font-weight: 600; + font-size: 13px; + font-family: Open Sans Semibold,Helvetica Neue,Helvetica,Arial,sans-serif; + -webkit-font-smoothing: antialiased; + color: #fff; + line-height: 20px; + +} +.container-fluid .book .meta .author { + font-weight: 600; + font-size: 13px; + font-family: Open Sans Semibold,Helvetica Neue,Helvetica,Arial,sans-serif; + -webkit-font-smoothing: antialiased; + color: hsla(0,0%,100%,.45); + line-height: 20px; +} +.container-fluid .book .meta .author > a { + font-weight: 600; + font-size: 13px; + font-family: Open Sans Semibold,Helvetica Neue,Helvetica,Arial,sans-serif; + -webkit-font-smoothing: antialiased; + color: hsla(0,0%,100%,.45); + line-height: 20px; +} +.container-fluid .book .cover img { + border: none; + -webkit-box-shadow: 0 0 2px rgba(0,0,0,.35); + -moz-box-shadow: 0 0 2px rgba(0,0,0,.35); + box-shadow: 0 0 2px rgba(0,0,0,.35); + position: relative; + z-index: -9; +} +.container-fluid .book .cover img:hover .container-fluid .book .cover:before {} + +#books > .cover > a, +#books_rand > .cover > a, +.book.isotope-item > .cover > a, +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 > div.col-sm-2 > a { + display: inline-block; + width: auto; + height: 100%; + +} +#books > .cover > a:hover, +#books_rand > .cover > a:hover, +.book.isotope-item > .cover > a:hover, +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 > div.col-sm-2 > a:hover { + outline: 2px solid var(--color-secondary); + font-size: 50px; + background: -webkit-radial-gradient(50% 50% farthest-corner,rgba(50,50,50,.5) 0,#323232 100%); + background: radial-gradient(farthest-corner at 50% 50%,rgba(50,50,50,.5) 50%,#323232 100%); + font-family: serif; + font-style: italic; +} +#books > .cover > a:hover:before, +#books_rand > .cover > a:hover:before, +.book.isotope-item > .cover > a:hover:before, +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 > div.col-sm-2 > a:hover:before { + content: "i"; + display: inline-block; + position: absolute; + background: var(--color-secondary); + color: #fff; + border-radius: 50%; + font-weight: 600; + font-size: 40px; + line-height: 50px; + width: 50px; + height: 50px; + padding-left: 19px; + left: 0; + right: 0; + top: 0; + bottom: 0; + margin: auto; +} +.container-fluid .book .cover { + width: auto; + display: inline-block; +} +.navbar { + margin-bottom: 0; + border: none; +} +.navbar-collapse.collapse { + height: 60px !important; + padding-bottom: 0; + overflow: visible!important; +} +.btn.active.focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn:active:focus, +.btn:focus { + outline: none; + outline-offset: 0; +} +.container-fluid .discover { + margin-bottom: 0; +} +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: 700; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: middle; + background-color: rgba(0,0,0,.15); + border-radius: 3px; +} +/* clean up */ + +app-loading-container { + background: #3f4245; +} +.app-loading>span { + display: none; +} +.app-loading { + margin: auto; + color: #fff !important; + text-align: center; + font-size: 20px; +} +.app-loading:before { + content: ''; + -webkit-box-sizing: border-box; + box-sizing: border-box; + border: 2px solid transparent; + border-radius: 50%; + border-top: 2px solid var(--color-secondary); + width: 24px; + height: 24px; + -webkit-animation: spin 0.5s linear infinite; + animation: spin 0.5s linear infinite; + display: block; + margin: 20px auto; +} +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + } +} +@keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +body > div.container-fluid > div > div.col-sm-10::-webkit-scrollbar, +body > div.container-fluid > div.row-fluid > div.col-sm-2::-webkit-scrollbar { + width: 14px; + max-height: calc(100% - 60px); +} +#meta-info::-webkit-scrollbar, +#description::-webkit-scrollbar, +.book-meta::-webkit-scrollbar { + width: 14px; +} +body > div.container-fluid > div > div.col-sm-10::-webkit-scrollbar-track, +body > div.container-fluid > div.row-fluid > div.col-sm-2::-webkit-scrollbar-track { + background-color: transparent; + max-height: calc(100% - 60px); +} +#meta-info::-webkit-scrollbar-track { + background-color: #202020; +} +#description::-webkit-scrollbar-track, +.book-meta::-webkit-scrollbar-track { + background-color: transparent; +} +body > div.container-fluid > div > div.col-sm-10::-webkit-scrollbar-thumb, +body > div.container-fluid > div.row-fluid > div.col-sm-2::-webkit-scrollbar-thumb { + min-height: 110px; + border: 3px solid transparent; + border-radius: 8px; + background-color: hsla(0,0%,100%,.2); + background-clip: padding-box; + max-height: calc(100% - 60px); +} +#meta-info::-webkit-scrollbar-thumb, +#description::-webkit-scrollbar-thumb, +.book-meta::-webkit-scrollbar-thumb { + min-height: 50px; + border: 3px solid transparent; + border-radius: 8px; + background-color: hsla(0,0%,100%,.2); + background-clip: padding-box; +} +body > div.container-fluid > div > div.col-sm-10::-webkit-scrollbar-thumb:hover, +body > div.container-fluid > div.row-fluid > div.col-sm-2::-webkit-scrollbar-thumb:hover, +#meta-info::-webkit-scrollbar-thumb:hover, +#description::-webkit-scrollbar-thumb:hover, +.book-meta::-webkit-scrollbar-thumb:hover { + background-color: hsla(0,0%,100%,.3); +} +::-webkit-scrollbar-corner { + background: transparent; +} +.pace-progress { + display: none; +} +.pace .pace-activity { + border: 2px solid transparent; + border-radius: 50%; + border-top: 2px solid var(--color-secondary); + width: 24px; + height: 24px; + right: auto; + left: 8px; + top: 17px; +} +body > .container-fluid { + margin: 0; + padding: 0; + width: calc(100%); + height: calc(100vh - 60px); + position: fixed; + top: 60px; + left: 0; + min-height: 1px; + min-width: 1px; +} +body > div.container-fluid > div.row-fluid > div.col-sm-2 { + position: fixed; + width: 240px; + height: calc(100vh - 120px); + left: 0; + top: 120px; + background: rgba(0,0,0,.15); + padding: 0 5px 20px 0; + overflow-y: auto; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 { + width: calc(100vw - 240px); + height: calc(100vh - 120px); + padding: 0; + position: fixed; + min-height: 1px; + min-width: 1px; + top: 120px; + right: 0; + overflow-y: scroll !important; +} + +body > div.container-fluid > div > div.col-sm-10 > div.discover > div { + overflow: visible !important; + width: 100%; + margin: 0; +} +body > div.container-fluid > div > div.col-sm-10 > div.discover { + height: auto; + width: calc(100vw - 254px); + margin: 40px 0 !important; + padding: 0 0 0 25px !important; + +} +body > div.container-fluid > div > div.col-sm-10 > div.single { + height: auto; + width: calc(100vw - 254px); + margin: 0 !important; + padding: 15px !important; + +} +body > div.container-fluid > div > div.col-sm-10 > div.discover { + margin-top: 0; +} +body > div.container-fluid > div > div.col-sm-10 > div.discover > h2 { + color: #eee !important; + font-size: 15px !important; + font-family: Open Sans Bold,Helvetica Neue,Helvetica,Arial,sans-serif !important; + line-height: 24px !important; + overflow: hidden!important; + min-width: 0; + width: 100% !important; + max-width: 100% !important; + text-overflow: ellipsis !important; + white-space: nowrap!important; + text-transform: uppercase !important; + font-weight: 600 !important; + padding-left: 15px !important; + margin-bottom: 0 !important; + position: relative !important; + top: 0 !important; + left: 0 !important; + height: auto !important; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 > h2 { + color: #eee; + font-size: 15px; + font-family: Open Sans Bold,Helvetica Neue,Helvetica,Arial,sans-serif; + line-height: 24px; + overflow: hidden; + min-width: 0; + width: 100%; + max-width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + text-transform: uppercase; + font-weight: 600; + padding-left: 15px; + margin-bottom: 20px; + margin-top: 40px; + position: relative; + top: 0; + left: 0; + height: auto; +} +body > div.container-fluid > div > div.col-sm-10 > h3:not(:first-of-type) { + color: #eee; + width: calc(100% - 30px); + max-width: 800px; + margin: 20px auto 10px; + text-align: right; +} +body > div.container-fluid > div > div.col-sm-10 > p { + margin: 40px 0 0 15px; +} +body > div.container-fluid > div > div.col-sm-10 > p > a { + color: var(--color-secondary); +} +body > div.container-fluid > div > div.col-sm-10 > p > a:hover { + color: #fff; +} +body > div.container-fluid > div > div.col-sm-10 > h3:first-of-type { + content: "About"; + width: calc(100vw - 240px); + height: 60px; + margin: 0; + padding-left: 25px; + color: hsla(0,0%,100%,.7); + font-family: "Open Sans Semibold", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 15px; + line-height: 60px; + text-size-adjust: 100%; + white-space: nowrap; + -webkit-box-direction: normal; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + font-weight: 600; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + position: fixed; + top: 60px; + left: 240px; + text-align: left; +} +body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-9.col-lg-9.book-meta > h2:before { + content: "Book Details"; + width: calc(100vw - 540px); + height: 60px; + margin: 0; + padding-left: 25px; + /* background: rgba(0,0,0,.15); */ + color: hsla(0,0%,100%,.7); + font-family: "Open Sans Semibold", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 15px; + line-height: 60px; + text-size-adjust: 100%; + white-space: nowrap; + -webkit-box-direction: normal; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + font-weight: 600; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + position: fixed; + top: 60px; + left: 240px; +} +body > div.container-fluid > div > div.col-sm-10 > div:nth-of-type(2) > h2:before { + content: "Books"; + width: calc(100vw - 240px); + height: 60px; + margin: 0; + padding-left: 25px; + /* background: rgba(0,0,0,.15); */ + color: hsla(0,0%,100%,.7); + font-family: "Open Sans Semibold", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 15px; + line-height: 60px; + text-size-adjust: 100%; + white-space: nowrap; + -webkit-box-direction: normal; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + font-weight: 600; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + position: fixed; + top: 60px; + left: 240px; +} +/* body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-9.col-lg-9.book-meta > h2, */ +.well > h2, +body > div.container-fluid > div > div.col-sm-10 > div.discover:only-of-type > h2 { + width: calc(100vw - 240px) !important; + height: 60px !important; + margin: 0; + padding-left: 25px !important; + color: hsla(0,0%,100%,.7) !important; + font-family: "Open Sans Semibold", "Helvetica Neue", Helvetica, Arial, sans-serif !important; + font-size: 15px !important; + line-height: 60px !important; + text-size-adjust: 100%; + white-space: nowrap !important; + -webkit-box-direction: normal; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + font-weight: 600 !important; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + position: fixed !important; + top: 60px !important; + left: 240px !important; + text-overflow: ellipsis !important; + max-width: calc(100vw - 550px) !important; + overflow: hidden !important; +} +body > div.container-fluid > div > div.col-sm-10 > h2 { + width: calc(100vw - 240px) !important; + height: 60px !important; + margin: 0; + padding-left: 25px !important; + color: hsla(0,0%,100%,.7) !important; + font-family: "Open Sans Semibold", "Helvetica Neue", Helvetica, Arial, sans-serif !important; + font-size: 15px !important; + line-height: 60px !important; + text-size-adjust: 100%; + white-space: nowrap !important; + -webkit-box-direction: normal; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + font-weight: 600 !important; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + position: fixed !important; + top: 60px !important; + left: 240px !important; + text-overflow: ellipsis !important; + max-width: calc(100vw - 550px) !important; + overflow: hidden !important; +} +/* body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-9.col-lg-9.book-meta > h2:before, +.well > h2:before */ +body > div.container-fluid > div.row-fluid > div.col-sm-10:before { + width: 100%; + height: 60px; + background: rgba(0,0,0,.15); + display: block; + content: ''; + position: fixed; + top: 60px; + left: 240px; +} +body > div.container-fluid > div > div.col-sm-10 > div.discover > h2, +body > div.container-fluid > div > div.col-sm-10 > h1, +body > div.container-fluid > div > div.col-sm-10 > div.discover > h1, +body > div.container-fluid > div > div.col-sm-10 > div.col-sm-6.col-lg-6.col-xs-6 > h2 { + width: calc(100vw - 240px); + height: 60px; + margin: 0; + padding-left: 25px; + /* background: rgba(0,0,0,.15); */ + color: hsla(0,0%,100%,.7); + font-family: "Open Sans Semibold", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 15px; + line-height: 60px; + text-size-adjust: 100%; + white-space: nowrap; + -webkit-box-direction: normal; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + font-weight: 600; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + position: fixed; + top: 60px; + left: 240px; +} +body > div.container-fluid > div > div.col-sm-10 > div.discover > h1:first-letter { + text-transform: uppercase; +} +body > div.container-fluid > div > div.col-sm-10 > div.discover > form > .btn.btn-default { + float: right; + margin: 0 0 0 10px; +} +.pagination { + display: block !important; + position: fixed; + top: 60px; + right: 0; + min-width: 1px; + min-height: 1px; + overflow: visible; + cursor: default; + height: 60px; + margin: 0; + line-height: 60px; + font-size: 15px; + z-index: 99999; +} +.pagination > a { + color: hsla(0,0%,100%,.7); +} +.pagination > a:hover { + color: #fff; + text-decoration: none; +} +.pagination > .ellipsis { + color: hsla(0,0%,100%,.7); +} +.navbar-default .navbar-nav>li>a:focus, +.navbar-default .navbar-nav>li>a:hover { + color: #fff !important; +} +body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.next { + color: rgba(0,0,0,0); + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: 100; + margin-left: 0px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + line-height: 60px; + height: 60px; + padding: 0; + font-size: 15px; + position: absolute; + top: 0; + right: 0; +} +body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.previous { + color: rgba(0,0,0,0); + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: 100; + margin-left: 0px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + line-height: 60px; + height: 60px; + width: 65px; + padding: 0; + font-size: 15px; + position: absolute; + top: 0; + right: 65px; +} +body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.next:before { + content: "\e092"; + visibility: visible; + color: hsla(0,0%,100%,.35); + height: 60px; + line-height: 60px; + border-left: 2px solid rgba(0,0,0,.3); + font-size: 15px; + padding: 20px 0 20px 25px; + margin-right: -27px; +} +body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.previous:before { + content: "\e091"; + visibility: visible; + color: hsla(0,0%,100%,.65); + height: 60px; + line-height: 60px; + + font-size: 15px; + padding: 20px 25px 20px 25px; + margin-right: ; +} +body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.next:hover:before { + color: #fff; +} +body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.previous:hover:before { + color: #fff; +} +.pagination > strong { + display: none; +} +.pagination:after { + content: "\e092"; + position: relative; + top: 0; + right: 0; + display: inline-block; + color: hsla(0,0%,100%,.55); + line-height: 60px; + + font-size: 15px; + padding: 0 25px; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: 100; + margin-left: 20px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + line-height: 60px; + height: 60px; + z-index: -1; +} +.pagination > .ellipsis { + display: none; +} +.pagination > a:nth-last-of-type(2) { + display: none; +} +body > div.container-fluid > div > div.col-sm-10 > div.container { + margin: 60px auto 120px; + border-top: 2px solid rgba(0,0,0,.3); + border-bottom: 2px solid rgba(0,0,0,.3); + background: hsla(0,0%,100%,.02); + padding: 0; + max-width: calc(100% - 30px); +} +body > div.container-fluid > div > div.col-sm-10 > div.container > div { + padding: 0; +} +body > div.container-fluid > div > div.col-sm-10 > div.container > div > .row { + padding: 4px 0; +} +body > div.container-fluid > div > div.col-sm-10 > div.container > div > .row:nth-of-type(odd) { + background: hsla(0,0%,100%,.02); +} +body > div.container-fluid > div > div.col-sm-10 > div.container > div > .row > .col-xs-6 { + width: calc(91.666% - 30px); + overflow: hidden; + text-overflow: ellipsis; + color: hsla(0, 0%, 100%, .45); +} +body > div.container-fluid > div > div.col-sm-10 > div.container > div > .row > .col-xs-6 > a { + text-overflow: ellipsis; + white-space: nowrap; + transition: all 0s; +} +/* Login */ +.well { + max-height: 520px; + height: auto; + background-color: transparent; + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + min-width: 600px; + max-width: 700px; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: 0 auto; + padding: 60px; +} +/* .well > h2 { + color: #eee; + text-align: center; +} */ +.well > form { + background: rgba(0,0,0,.4); + color: hsla(0,0%,100%,.45); + text-transform: uppercase; + padding: 60px; + margin-top: 60px; +} +.checkbox { + text-transform: none; +} +.well > form > .btn { + display: inline-block; + overflow: hidden; + border-radius: 4px; + color: #fff; + vertical-align: middle; + text-align: center; + text-transform: uppercase; + white-space: nowrap; + font-family: Open Sans Bold,Helvetica Neue,Helvetica,Arial,sans-serif; + -webkit-transition: background-color 0.2s,color 0.2s; + transition: background-color 0.2s,color 0.2s; + padding: 6px 30px 5px; + font-size: 15px; + font-weight: 600; + background-color: var(--color-secondary); + border: none; + width: 100%; + text-rendering: auto; + letter-spacing: normal; + word-spacing: normal; + text-indent: 0; + text-shadow: none; + margin-top: 20px; +} +.well > form > .btn:hover { + background-color: var(--color-secondary-hover); +} +/* Login */ + +/* Alert */ +body > div.row-fluid { + margin-top: 0 !important; +} +.alert { + position: fixed; + top: auto; + bottom: 20px; + left: 50%; + width: 50%; + margin: 0; + -webkit-transform: translate(-50%); + -ms-transform: translate(-50%); + transform: translate(-50%); + opacity: 1 !important; + right: 0; + border-radius: 10px; + + background-color: #000; + color: #eee; + border: 0; + -webkit-box-shadow: 0 4px 12px rgba(0,0,0,.5); + box-shadow: 0 4px 12px rgba(0,0,0,.5); + -webkit-transition: all 0.5s; + transition: all 0.5s; + + height: 34px; + line-height: 1.71428571 !important; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 14px; + padding: 5px 0; + + z-index: 99999; + -moz-animation: cssAnimation 0s ease-in 10s forwards; + /* Firefox */ + -webkit-animation: cssAnimation 0s ease-in 10s forwards; + /* Safari and Chrome */ + -o-animation: cssAnimation 0s ease-in 10s forwards; + /* Opera */ + animation: cssAnimation 0s ease-in 10s forwards; + -webkit-animation-fill-mode: forwards; + animation-fill-mode: forwards; + +} +@keyframes cssAnimation { + to { + width: 0; + height: 0; + overflow: hidden; + } +} +@-webkit-keyframes cssAnimation { + to { + width: 0; + height: 0; + visibility: hidden; + } +} + +.alert-danger { + background-color: rgba(255,85,51,.3); + color: #fff; +} +/* Alert */ + +.table>thead>tr>th { + border-bottom: 0; +} +.table>tbody>tr>td, +.table>tbody>tr>th, +.table>tfoot>tr>td, +.table>tfoot>tr>th, +.table>thead>tr>td, +.table>thead>tr>th { + border-top: 1px solid rgba(0,0,0,.3); + background: rgba(0,0,0,.15); +} +#stats, +#libs { + margin: auto; + width: calc(100% - 30px); + max-width: 800px; + color: hsla(0,0%,100%,.75); + border-bottom: 2px solid rgba(0,0,0,.3); + border-top: 2px solid rgba(0,0,0,.2); +} +#libs { + margin-bottom: 40px; +} +#stats > tbody > tr > th, +#libs > tbody > tr > th, +#libs > thead > tr > th:first-child { + color: hsla(0,0%,100%,.45); +} +#books .cover img, +#books_rand .cover img, +.book .cover img { + width: auto; +} +.container-fluid img { + display: block; + width: 100%; + height: auto; + max-width: 195px; + margin: auto; +} +.container-fluid .single .cover img { + border: 0; + border-radius: 0; + -webkit-box-shadow: 0 0 2px rgba(0,0,0,.35); + -moz-box-shadow: 0 0 2px rgba(0,0,0,.35); + box-shadow: 0 0 2px rgba(0,0,0,.35); + background-color: rgba(0,0,0,.45); +} + + +body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col-lg-3, +body > div.container-fluid > div > div.col-sm-10 > div.col-sm-3.col-lg-3.col-xs-12 { + max-width: calc(25% - 75px); + position: fixed; +} +body > div.container-fluid > div > div.col-sm-10 > div.col-sm-3.col-lg-3.col-xs-12 { + padding-top: 40px; + padding-bottom: 40px; +} +body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-9.col-lg-9.book-meta, +body > div.container-fluid > div > div.col-sm-10 > div.col-sm-8 { + margin-left: calc(25% + 3.3333px); + width: calc(75% + 10.5px); +} +body > div.container-fluid > div > div.col-sm-10 > div.col-sm-8 { + padding-top: 40px; + padding-bottom: 40px; +} +body > div.container-fluid > div > div.col-sm-10 > div.col-sm-8 > form > div > span > input.form-control.typeahead.tt-hint { + background: hsla(0, 0%, 100%, .25) !important; +} +body > div.container-fluid > div > div.col-sm-10 > div.col-sm-8 > form > div > span > input.form-control:focus { + background: #fff !important; +} +.glyphicon-star { + color: #fff; +} +.rating-input .glyphicon:hover { + cursor: pointer; +} +.rating-input:hover { + cursor: default; +} +.rating-input { + padding-left: 0; +} +body > div.container-fluid > div > div.col-sm-10 > div.col-sm-8 > form > .btn.btn-default { + float: right; + margin: 0 0 0 10px; +} +#get_meta { + float: left; + margin: 0; +} +textarea { + resize: none; + /* Safari */ + resize: vertical; + /* Chrome */ +} +.modal-backdrop.in { + display: none; +} +#metaModal, +.modal.fade.in {} +#metaModal { + top: 0; + overflow: hidden; + padding-top: 60px; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,.5); +} +#metaModal > .modal-dialog > .modal-content { + z-index: 9999999999999999999999; +} +#metaModal > .modal-dialog > .modal-content { + max-height: calc(100vh - 90px); + -webkit-box-shadow: 0 5px 15px rgba(0,0,0,.5); + box-shadow: 0 5px 15px rgba(0,0,0,.5); + border-radius: 3px; +} +#metaModal > .modal-dialog > .modal-content > .modal-header { + padding: 0; + background: #282828; + border-radius: 3px 3px 0 0; +} +#metaModal > .modal-dialog > .modal-content > .modal-header > h4 { + border-bottom: 0; + background: #323232; + height: 65px; + + padding: 15px 0 0 15px; + font-size: 20px; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.71428571; + font-family: Open Sans Regular,Helvetica Neue,Helvetica,Arial,sans-serif; + font-weight: 400; + color: #eee; + border-radius: 3px 3px 0 0; + margin-bottom: 0; +} +#metaModal > .modal-dialog > .modal-content > .modal-header > h4:before { + content: "\E025"; + font: normal normal normal 16px/1 Glyphicons Halflings; + color: #999; + + padding-right: 10px; +} +#metaModal > .modal-dialog > .modal-content > .modal-header > .close { + color: #eee; + opacity: 0.2; + font-size: 20px; + text-shadow: none; + margin: 20px; +} +#metaModal > .modal-dialog > .modal-content > .modal-header > .close:hover { + opacity: 0.5; +} +#metaModal > .modal-dialog > .modal-content > .modal-header > #meta-search { + margin: 25px 0 0 0; + +} +#metaModal > .modal-dialog > .modal-content > .modal-header > #meta-search > .input-group { + max-width: 100%; +} +#metaModal > .modal-dialog > .modal-content > .modal-header > #meta-search > .input-group > input { + width: calc(100% - 51px); + border-radius: 0; + background-color: hsla(0, 0%, 100%, .15); +} +#metaModal > .modal-dialog > .modal-content > .modal-header > #meta-search > .input-group { + width: 100%; +} +#metaModal > .modal-dialog > .modal-content > .modal-header > #meta-search > .input-group > span > button { + height: 38px; + width: 50px; + padding: 0; + font-weight: 100; + font-family: "Open Sans Semibold", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px !important; + font-stretch: 100%; + font-style: normal; + font-variant-caps: normal; + font-variant-east-asian: normal; + font-variant-ligatures: normal; + font-variant-numeric: normal; + font-weight: 400; + letter-spacing: normal; + line-height: 22.2857px !important; + color: #eee; + vertical-align: middle; + background-color: hsla(0, 0%, 100%, .15); + border-radius: 0; + margin: 0 0 0 -1px; +} +#metaModal > .modal-dialog > .modal-content > .modal-header > #meta-search > .input-group > span { + float: right; + text-align: right; + width: 50px; + + margin: 0; +} +#metaModal > .modal-dialog > .modal-content > .modal-header > div { + font-family: Open Sans Regular,Helvetica Neue,Helvetica,Arial,sans-serif; + font-size: 12px; + line-height: 1.71428571; + color: var(--color-primary); + font-weight: 100; + text-align: right; + position: absolute; + right: 0; + padding: 12.5px; +} +#metaModal > .modal-dialog > .modal-content > .modal-body { + padding: 0; +} +#metaModal > .modal-dialog > .modal-content > .modal-body > .text-center { + text-align: left; + margin-bottom: 0; + padding-top: 10px; +} +#metaModal > .modal-dialog > .modal-content > .modal-body > #book-list { + margin-bottom: 0; +} +input.pill:checked + label { + background-color: transparent; + border-color: transparent; + color: var(--color-secondary); +} + +input.pill + label { + border: 0; + border-radius: 0; + color: hsla(0,0%,100%,.25); + cursor: pointer; + display: inline-block; + padding: 3px 15px; + user-select: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +input.pill + label:hover { + color: var(--color-secondary-hover); +} + + +#meta-info { + max-height: calc(100vh - 357px); + overflow-y: scroll; + background: #282828 !important; + padding: 0; + margin: 0; +} +#meta-info #book-list .media > .media-body { + font-family: "Open Sans Regular", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 24px; + color: #999; + font-weight: 100; + padding-left: 15px; +} +#meta-info #book-list .media > img { + padding: 0; +} +#meta-info #book-list .media > .media-body > h4 > a { + color: #eee; +} +#meta-info #book-list .media > .media-body > h4 > a:hover { + color: #fff; +} +#meta-info #book-list { + width: 100%; + margin: 0; +} +#meta-info #book-list .media:nth-of-type(odd) { + background: hsla(0, 0%, 100%, .02); +} + +#meta-info #book-list .media { + margin-top: 0; + padding: 20px 15px 5px; +} +#meta-info #book-list .media > .media-body > p > a { + color: var(--color-secondary); + padding-left: 10px; +} +#meta-info #book-list .media > .media-body > p > a:hover { + color: #fff; +} + +.modal-content { + position: relative; + background-color: #323232; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 0; + border-radius: 0 0 3px 3px; + outline: 0; + -webkit-box-shadow: 0 3px 9px rgba(0,0,0,.5); + box-shadow: 0 3px 9px rgba(0,0,0,.5); +} +.modal-footer { + padding: 15px; + text-align: right; + border-top: 0; +} +.modal-header { + padding: 15px; + border-bottom: 0; +} +#meta-info #book-list .media > img:hover { + cursor: pointer; + outline: 2px solid var(--color-secondary); + filter:brightness(90%); +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-9 > div:nth-child(10) > label:before { + content: "Edit Metadata"; + width: calc(100vw - 240px); + height: 60px; + margin: 0; + padding-left: 25px; + background: transparent; + color: hsla(0,0%,100%,.7); + font-family: "Open Sans Semibold", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 15px; + line-height: 60px; + text-size-adjust: 100%; + white-space: nowrap; + -webkit-box-direction: normal; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + font-weight: 600; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + position: fixed; + top: 60px; + left: 240px; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-xs-12 { + margin: 50px 0; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form > #test:before { + content: "Advanced Search"; + width: calc(100vw - 240px); + height: 60px; + margin: 0; + padding-left: 25px; + background: transparent; + color: hsla(0,0%,100%,.7); + font-family: "Open Sans Semibold", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 15px; + line-height: 60px; + text-size-adjust: 100%; + white-space: nowrap; + -webkit-box-direction: normal; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + font-weight: 600; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + position: fixed; + top: 60px; + left: 240px; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form > #test { + border-top: 2px solid rgba(0,0,0,.3); + margin-top: -50px; + padding-top: 50px; + padding-left: 33.33%; + margin-left: -33.33%; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form > .form-group > .btn-toolbar-lg > label.btn { + margin: 2px 3px 0 0; + padding: 0 10px; + background: hsla(0,0%,100%,.25); +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form > .form-group > .btn-toolbar-lg > label.btn:hover { + background: hsla(0,0%,100%,.3); +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form > .form-group > .btn-toolbar-lg > label.btn-danger:hover { + background: #ce3d2a; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form > .form-group > .btn-toolbar-lg > label.btn-danger.active { + background: #ac3323; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form > .form-group > .btn-toolbar-lg > label.btn-danger.active:hover { + background: #ce3d2a; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form > .form-group > .btn-toolbar-lg > label.btn-primary:hover { + background: var(--color-secondary-hover); +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form > .form-group > .btn-toolbar-lg > label.btn-primary.active { + background: var(--color-secondary); +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form > .form-group > .btn-toolbar-lg > label.btn-primary.active:hover { + background: var(--color-secondary-hover); +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form > .form-group > .btn-toolbar-lg { + margin-left: -33.33%; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form > label { + margin-left: -33.33%; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form > label:first-of-type { + padding-top: 40px; +} + + + + + +#bookDetailsModal .modal-header > .close { + display: none; +} +#bookDetailsModal .modal-footer { + /* width: 100px; + position: fixed; + top: 800px; + left: 0; */ + display: none; +} +a:focus { + outline: none; + outline-offset: 0; +} + +#bookDetailsModal { + width: calc(100vw - 240px); + height: calc(100vh - 60px); + padding: 0; + position: fixed; + min-height: 1px; + min-width: 1px; + top: 60px; + left: 240px; + + background-image: url(https://raw.githubusercontent.com/leram84/layer.Cake/master/Resources/blur-noise.png), url("https://raw.githubusercontent.com/leram84/layer.Cake/master/Resources/blur-light.png"); + background-repeat: repeat, no-repeat; + background-attachment: fixed, fixed; + background-position: center center, center center; + background-size: auto, cover !important; + -webkit-background-size: auto, cover; + -moz-background-size: auto, cover; + -o-background-size: auto, cover; +} +#bookDetailsModal > .modal-dialog.modal-lg { + padding: 0; + margin: 0; + width: 100%; + height: 100%; + + +} +#bookDetailsModal > .modal-dialog.modal-lg > .modal-content { + background-color: transparent; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; +} +#bookDetailsModal > .modal-dialog.modal-lg > .modal-content > .modal-header { + width: 100%; + height: 60px; + margin: 0; + padding: 0 0 0 25px; + background: rgba(0,0,0,.15); + color: hsla(0,0%,100%,.7); + font-family: "Open Sans Semibold", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 15px; + line-height: 60px; + text-size-adjust: 100%; + white-space: nowrap; + -webkit-box-direction: normal; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + font-weight: 600; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + position: fixed; + top: 0; + left: 0; +} +#bookDetailsModal > .modal-dialog.modal-lg > .modal-content > .modal-header > h4 { + width: 100%; + max-width: calc(100% - 340px); + height: 60px; + line-height: 60px; +} +#bookDetailsModal > .modal-dialog.modal-lg > .modal-content > .modal-body { + width: calc(100vw - 240px); + height: calc(100vh - 120px); + position: fixed; + min-height: 1px; + min-width: 1px; + top: 60px; + left: 0; + padding: 0; +} +#bookDetailsModal > .modal-dialog.modal-lg > .modal-content > .modal-body > .container-fluid { + padding: 0; +} +#bookDetailsModal > .modal-dialog.modal-lg > .modal-content > .modal-body> div > div > div > div.col-sm-3.col-lg-3.col-xs-5 { + padding: 30px 15px 15px 30px; + height: 300px; +} +body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col-lg-3.col-xs-5 { + padding: 15px; + height: 300px; +} +#bookDetailsModal > .modal-dialog.modal-lg > .modal-content > .modal-body> div > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover, +body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover { + margin: 0; + width: 100%; + height: 100%; + +} +#bookDetailsModal > .modal-dialog.modal-lg > .modal-content > .modal-body> div > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover > img, +body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover > img { + margin: 0; + max-height: 300px; +} + + + + + + + + + + + + + + + + +#bookDetailsModal .book-meta > h2 { + margin-top: 15px; +} + +.book-meta > h2 { + font-size: 24px; + font-family: Open Sans Bold,Helvetica Neue,Helvetica,Arial,sans-serif; + color: #eee; + font-weight: 600; + line-height: 1; + margin: 0; + max-width: calc(100% - 100px); + padding: 15px 0; +} +.book-meta > .author:before { + content: "Author:"; + padding-right: 4px; + color: #eee; +} +.book-meta > .author > a { + color: #eee; +} +.book-meta > .rating { + float: right; + position: relative; + margin-top: -60px; + z-index: -1; +} +.book-meta > p { + color: hsla(0, 0%, 100%, .45); +} +.book-meta > .rating > p > .glyphicon-star { + color: #eee; +} +.book-meta > .languages > p > .label-default { + background: transparent; + color: hsla(0, 0%, 100%, .45); + padding: 0; + font-size: 14px; +} +.book-meta > .languages > p:first-letter { + text-transform: uppercase; +} +.book-meta > .tags .btn-info { + background-color: rgba(0,0,0,.15); + color: hsla(0,0%,100%,.7); + padding: 0 10px; + font-size: 13px; + display: inline-block; + overflow: hidden; + border-radius: 4px; + vertical-align: middle; + text-align: center; + white-space: nowrap; + font-family: Open Sans Bold,Helvetica Neue,Helvetica,Arial,sans-serif; + -webkit-transition: background-color 0.2s,color 0.2s; + transition: background-color 0.2s,color 0.2s; + margin: 2px 3px 0 0; +} +.book-meta > .tags .btn-info:hover { + color: #fff; + text-decoration: underline; +} +.book-meta > .tags { + padding-left: 40px; +} +.book-meta > .tags a{ + max-width: 100%; + text-overflow: ellipsis; +} +.book-meta > .tags .glyphicon-tags { + margin-left: -40px; + margin-right: 17px; + color: hsla(0, 0%, 100%, .45); +} +.book-meta > h3:first-of-type { + font-size: 14px; + font-weight: 600; + text-transform: uppercase; + margin-top: -20px; + border-top: 2px solid rgba(0,0,0,.3); + padding-top: 20px; + margin-top: 20px; +} +.book-meta > div { + color: hsla(0,0%,100%,.65); +} +.more-stuff { + margin-top: 0; + padding-top: 0; + border-top: 0; +} +.book-meta > .more-stuff > br { + display: none; +} +.book-meta > .more-stuff > .btn-toolbar:not(#shelf-actions) { + position: fixed; + top: 0; + right: 240px; +} +.col-sm-10 .book-meta > .more-stuff > .btn-toolbar:not(#shelf-actions) { + position: fixed; + top: 60px; + right: 240px; +} +#shelf-actions { + position: fixed; + top: 415px; + left: 0; + padding: 0 15px 0 30px; + margin: 0; + width: calc(25%); +} +.col-sm-10 #shelf-actions { + left: 240px; + top: 475px; + width: calc(25% - 60px); +} +#shelf-actions .btn-group { + width: 100%; +} +.book-meta > .more-stuff > .btn-toolbar { + +} +.book-meta > .more-stuff > .btn-toolbar:nth-of-type(2) { + +} +.book-meta .btn-toolbar > .btn-group > .btn-warning, +.btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2, +#btnGroupDrop1, +#sendbtn, +#read-in-browser, +.btn-toolbar > .btn-group > #btnGroupDrop2, +.book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type { + background: transparent; + color: transparent; + width: 60px; + height: 60px; + margin: 0; + overflow: hidden; + +} +.book-meta .btn-toolbar > .btn-group > .btn-warning > span, +.btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span, +#btnGroupDrop1 > span, +#sendbtn > span, +#read-in-browser > span, +.btn-toolbar > .btn-group > #btnGroupDrop2 > span, +.book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type > span { + color: hsla(0,0%,100%,.7); + +} +.book-meta .btn-toolbar > .btn-group > .btn-warning:hover > span, +.btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2:hover > span, +#btnGroupDrop1:hover > span, +#sendbtn:hover > span, +#read-in-browser:hover > span, +.btn-toolbar > .btn-group > #btnGroupDrop2:hover > span, +.book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type:hover > span { + color: #fff; +} +.btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.caret { + margin-left: -135px; + padding-bottom: 5px; +} +.btn-toolbar > .btn-group > #btnGroupDrop2 > span.caret, +#read-in-browser > span.caret { + margin-left: -135px; + padding-bottom: 5px; +} +#btnGroupDrop1 > span.caret { + margin-left: -85px; + padding-bottom: 5px; +} +.book-meta .btn-toolbar > .btn-group { + margin: 0; +} +.book-meta > div.btn-toolbar > div > div:nth-child(1) > button:not(#btnGroupDrop1) { + display: none; +} +.book-meta .btn-toolbar > .btn-group > .btn-warning > span.glyphicon-edit, +#sendbtn > span, +.book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type > span { + font-size: 18px; + line-height: 42px; + width: 100%; + +} +.btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-eye-open:before, +#read-in-browser > span.glyphicon-eye-open:before { + content: "\e164"; + font-size: 18px; +} +.btn-toolbar > .btn-group > #btnGroupDrop2 > span.glyphicon-list:before, +#read-in-browser > span.glyphicon-list:before { + font-size: 18px; +} +#btnGroupDrop1 > span.glyphicon-download:before { + font-size: 18px; +} +.book-meta > div.btn-toolbar { + position: fixed; + top: 0; + right: 0; + margin: 0; +} +.col-sm-10 .book-meta > div.btn-toolbar { + position: fixed; + top: 60px; + right: 0; + margin: 0; +} +.book-meta > div.btn-toolbar ul.dropdown-menu { + margin-left: -110px; +} +#bookDetailsModal > div > div > div.modal-body > div > div > div > div.col-sm-9.col-lg-9.book-meta > div.btn-toolbar > div > div.btn-group.open > div.dropdown-backdrop { + display: none; +} +#add-to-shelf { + margin-bottom: 0; + padding: 6px 7px; + line-height: 1.2; + border-radius: 3px; + font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif; + border: 0; + -webkit-transition: background-color 0.1s; + transition: background-color 0.1s; + font-weight: 600; + white-space: nowrap; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; +} +#add-to-shelf > span.glyphicon.glyphicon-list:before { + content: "+"; + font-weight: 100 !important; + font-size: 12px; + padding: 0 3px; +} +.btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open .dropdown-toggle.btn-primary { + background-color: hsla(0, 0%, 100%, .3); +} +.btn-primary.active.focus, .btn-primary.active:focus, .btn-primary.active:hover, .btn-primary:active.focus, .btn-primary:active:focus, .btn-primary:active:hover, .open>.dropdown-toggle.btn-primary.focus, .open>.dropdown-toggle.btn-primary:focus, .open>.dropdown-toggle.btn-primary:hover { + color: #fff; + background-color: hsla(0, 0%, 100%, .3); + border-color: transparent; +} + +.book-meta > div.more-stuff > .btn-toolbar > .btn-group > .btn-group > .dropdown-menu { + margin-left: -100px; +} +.book-meta .btn-toolbar > .btn-group > ul.dropdown-menu { + margin-left: -200px; + max-width: 200px; +} +.book-meta .btn-toolbar > .btn-group > ul.dropdown-menu { + margin-left: 0px; + max-width: 200px; + overflow: hidden; +} +.book-meta .btn-toolbar > .btn-group > ul.dropdown-menu > li > a { + max-width: 200px; + text-overflow: ellipsis; + overflow: hidden; +} +.dropdown-menu { + background-color: #020a0e !important; + -webkit-box-shadow: 0 4px 10px rgba(0, 0, 0, .35); + box-shadow: 0 4px 10px rgba(0, 0, 0, .35); + border-radius: 4px !important; +} +.dropdown-menu > li > a { + color: hsla(0,0%,100%,.7); + font-size: 13px; + font-stretch: 100%; + font-style: normal; + font-variant-caps: normal; + font-variant-east-asian: normal; + font-variant-ligatures: normal; + font-variant-numeric: normal; + font-weight: 600; +} +.dropdown-menu > li > a:hover { + color: #fff; + background: hsla(0, 0%, 100%, .08); +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: none; + box-shadow: none; +} +.book-meta > .identifiers > p > .btn-success { + background-color: rgba(0,0,0,.15); + color: hsla(0,0%,100%,.7); + padding: 0 10px; + font-size: 13px; + display: inline-block; + overflow: hidden; + border-radius: 4px; + vertical-align: middle; + text-align: center; + white-space: nowrap; + font-family: Open Sans Bold,Helvetica Neue,Helvetica,Arial,sans-serif; + -webkit-transition: background-color 0.2s,color 0.2s; + transition: background-color 0.2s,color 0.2s; + margin: 2px 3px 0 22px; +} +.book-meta > .identifiers > p > .btn-success:hover { + color: #fff; + text-decoration: underline; +} +#bookDetailsModal .book-meta { + color: hsla(0,0%,100%,.7); + height: calc(100vh - 120px); + overflow-x: hidden; + overflow-y: scroll; +} +.book-meta .publishers { + color: hsla(0,0%,100%,.45); +} +.book-meta > .identifiers > p > .glyphicon-link { + color: hsla(0,0%,100%,.45); +} +.rating .glyphicon-star.good { + color: var(--color-primary); +} +body > div.container-fluid > div > div.col-sm-10 > div.discover > .btn-danger { + background: #ac3323; + width: auto; +} +body > div.container-fluid > div > div.col-sm-10 > div.discover > .btn-danger:hover { + background: #ce3d2a; +} +body > div.container-fluid > div > div.col-sm-10 > div.discover > .btn-primary:first-of-type { + background: hsla(0, 0%, 100%, .25); +} +body > div.container-fluid > div > div.col-sm-10 > div.discover > .btn-primary:first-of-type:hover { + background: hsla(0, 0%, 100%, .3); +} +body > div.container-fluid > div > div.col-sm-10 > div.discover > .btn-primary:last-of-type { + background: var(--color-secondary); +} +body > div.container-fluid > div > div.col-sm-10 > div.discover > .btn-primary:last-of-type:hover { + background: var(--color-secondary-hover); +} +.book-meta > div.more-stuff > .btn-toolbar > .btn-group[aria-label="Remove from shelves"] { + max-width: 100%; + +} +.book-meta > div.more-stuff > .btn-toolbar > .btn-group[aria-label="Remove from shelves"] > a { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + margin: 20px 0 0 0 !important; + border-radius: 3px; + background: hsla(0, 0%, 100%, .25); + text-transform: none; + padding: 0px 10px 0px 10px; + text-align: left; + line-height: 1.5; +} +.book-meta > div.more-stuff > .btn-toolbar > .btn-group[aria-label="Remove from shelves"] > a:hover { + background: #ce3d2a; + color: #fff; +} +.book-meta > div.more-stuff > .btn-toolbar > .btn-group[aria-label="Remove from shelves"] > a > .glyphicon-remove { + font-size: 12px; + padding-right: 3px; + +} +/* body > div.container-fluid > div > div.col-sm-10 > div.single > div.row > div.col-sm-3.col-lg-3.col-xs-5 > div.cover > img { + max-height: 300px; + max-width: 100%; + width: auto; + margin: 0; +} */ +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form { + padding-right: 25px; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.form-group { + max-width: 800px; + padding-left: 15px; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 { + padding: 0; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 { + padding: 0; + margin-bottom: 60px; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > #submit { + float: left; + margin-left: calc(100vw - 400px) +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 > div.col-sm-2 { + +} +.dropdown-backdrop { + display: none; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-9.col-xs-12 > button.btn.btn-default { + float: right; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-9.col-xs-12 > a:last-of-type { + float: right; + background: hsla(0, 0%, 100%, .25); + margin-right: 10px; +} +body > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-9.col-xs-12 > a:last-of-type:hover { + background: hsla(0, 0%, 100%, .3); +} diff --git a/cps/static/css/fonts/glyphicons-halflings-regular.eot b/cps/static/css/fonts/glyphicons-halflings-regular.eot index 87eaa434..b93a4953 100644 Binary files a/cps/static/css/fonts/glyphicons-halflings-regular.eot and b/cps/static/css/fonts/glyphicons-halflings-regular.eot differ diff --git a/cps/static/css/fonts/glyphicons-halflings-regular.svg b/cps/static/css/fonts/glyphicons-halflings-regular.svg index 5fee0685..94fb5490 100644 --- a/cps/static/css/fonts/glyphicons-halflings-regular.svg +++ b/cps/static/css/fonts/glyphicons-halflings-regular.svg @@ -6,223 +6,283 @@ - - + + - - + + - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cps/static/css/fonts/glyphicons-halflings-regular.ttf b/cps/static/css/fonts/glyphicons-halflings-regular.ttf index be784dc1..1413fc60 100644 Binary files a/cps/static/css/fonts/glyphicons-halflings-regular.ttf and b/cps/static/css/fonts/glyphicons-halflings-regular.ttf differ diff --git a/cps/static/css/fonts/glyphicons-halflings-regular.woff b/cps/static/css/fonts/glyphicons-halflings-regular.woff index 2cc3e485..9e612858 100644 Binary files a/cps/static/css/fonts/glyphicons-halflings-regular.woff and b/cps/static/css/fonts/glyphicons-halflings-regular.woff differ diff --git a/cps/static/css/kthoom.css b/cps/static/css/kthoom.css index a6b41a32..770b94a2 100644 --- a/cps/static/css/kthoom.css +++ b/cps/static/css/kthoom.css @@ -1,32 +1,133 @@ body { - background: #444; - overflow: hidden; - color: white; - font-family: sans-serif; - margin: 0px; + background: #444; + overflow-x: hidden; + overflow-y: auto; + color: white; + font-family: sans-serif; + margin: 0px; } -.main { - position: re; - left: 5px; - overflow: hidden; - right: 5px; - text-align: center; - top: 5px; +#main { + text-align: center; + z-index: 2; +} + +.view { + padding-top:0px; +} + +#sidebar a, +#sidebar ul, +#sidebar li, +#sidebar li img { + max-width: 100%; + text-align: center; +} + +#sidebar ul { + position: relative; +} + +#sidebar a { + display: inline-block; + position: relative; + cursor: pointer; + padding: 4px; + + transition: all .2s ease; +} + +#sidebar a:hover, +#sidebar a:focus { + outline: none; + box-shadow: 0px 2px 8px 1px black; +} + +#sidebar a.active, +#sidebar a.active img + span { + background-color: #45B29D; +} + +#sidebar li img { + display: block; + max-height: 200px; +} + +#sidebar li img + span { + position: absolute; + bottom: 0; + right: 0; + padding: 2px; + min-width: 25px; + line-height: 25px; + background: #6b6b6b; + border-top-left-radius: 5px; +} + +#sidebar #panels { + z-index: 1; } #progress { - position: absolute; - display: inline; - left: 90px; - right: 160px; - height: 20px; - margin-top: 1px; - text-align: right; + position: absolute; + display: inline; + top: 0; + left: 0; + right: 0; + min-height: 4px; + font-family: sans-serif; + font-size: 10px; + line-height: 10px; + text-align: right; + + transition: min-height 150ms ease-in-out; +} + +#progress .bar-load, +#progress .bar-read { + display: flex; + align-items: flex-end; + justify-content: flex-end; + position: absolute; + top: 0; + left: 0; + bottom: 0; + + transition: width 150ms ease-in-out; +} + +#progress .bar-load { + color: #000; + background-color: #CCC; +} + +#progress .bar-read { + color: #FFF; + background-color: #45B29D; +} + +#progress .text { + display: none; + padding: 0 5px; +} + +#progress.loading, +#titlebar:hover #progress { + min-height: 10px; +} + +#progress.loading .text, +#titlebar:hover #progress .text { + display: inline-block; } .hide { - display: none !important; + display: none !important; +} + +#mainContent { + overflow: auto; + outline: none; } #mainText { @@ -42,29 +143,13 @@ body { word-wrap: break-word; } -#mainImage{ - margin-top: 32px; +#titlebar { + min-height: 25px; + height: auto; } -#titlebar.main { - opacity: 0; - position: absolute; - top: 0; - height: 30px; - left: 0; - right: 0; - background-color: black; - padding-bottom: 70px; - -webkit-transition: opacity 0.2s ease; - -moz-transition: opacity 0.2s ease; - transition: opacity 0.2s ease; - background: -moz-linear-gradient(top, rgba(0,2,34,1) 0%, rgba(0,1,24,1) 30%, rgba(0,0,0,0) 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(0,2,34,1)), color-stop(30%,rgba(0,1,24,1)), color-stop(100%,rgba(0,0,0,0))); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, rgba(0,2,34,1) 0%,rgba(0,1,24,1) 30%,rgba(0,0,0,0) 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, rgba(0,2,34,1) 0%,rgba(0,1,24,1) 30%,rgba(0,0,0,0) 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, rgba(0,2,34,1) 0%,rgba(0,1,24,1) 30%,rgba(0,0,0,0) 100%); /* IE10+ */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#000222', endColorstr='#00000000',GradientType=0 ); /* IE6-9 */ - background: linear-gradient(top, rgba(0,2,34,1) 0%,rgba(0,1,24,1) 30%,rgba(0,0,0,0) 100%); /* W3C */ +#metainfo { + max-width: 70%; } #prev { @@ -100,6 +185,72 @@ body { color: #000; } +th, td { + padding: 5px; +} + +th { + text-align: right; + vertical-align: top; +} + +.modal { + /* Makes the modal responsive. Note sure if this should be moved to main.css */ + margin: 0; + max-width: 96%; + transform: translate(-50%, -50%); +} +.md-content { + min-height: 320px; + height: auto; +} +.md-content > div { + overflow: hidden; +} +.md-content > div p { + padding: 5px 0; +} + +.settings-column { + float: left; + min-width: 35%; + padding-bottom: 10px; +} + +.inputs { + margin: -5px; +} + +.inputs input { + vertical-align: middle; +} + +.inputs label { + display: inline-block; + margin: 5px; + white-space: nowrap; +} + +.dark-theme #main { + background-color: #000; +} + +.dark-theme #titlebar { + color: #DDD; +} + +.dark-theme #titlebar a:active { + color: #FFF; +} + +.dark-theme #progress .bar-read { + background-color: red; +} + + +.dark-theme .overlay { + background-color: rgba(0,0,0,0.8); +} diff --git a/cps/static/css/libs/bootstrap-editable.css b/cps/static/css/libs/bootstrap-editable.css new file mode 100644 index 00000000..ff7ea50f --- /dev/null +++ b/cps/static/css/libs/bootstrap-editable.css @@ -0,0 +1,663 @@ +/*! X-editable - v1.5.3 +* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery +* http://github.com/vitalets/x-editable +* Copyright (c) 2015 Vitaliy Potapov; Licensed MIT */ +.editableform { + margin-bottom: 0; /* overwrites bootstrap margin */ +} + +.editableform .control-group { + margin-bottom: 0; /* overwrites bootstrap margin */ + white-space: nowrap; /* prevent wrapping buttons on new line */ + line-height: 20px; /* overwriting bootstrap line-height. See #133 */ +} + +/* + BS3 width:1005 for inputs breaks editable form in popup + See: https://github.com/vitalets/x-editable/issues/393 +*/ +.editableform .form-control { + width: auto; +} + +.editable-buttons { + display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */ + vertical-align: top; + margin-left: 7px; + /* inline-block emulation for IE7*/ + zoom: 1; + *display: inline; +} + +.editable-buttons.editable-buttons-bottom { + display: block; + margin-top: 7px; + margin-left: 0; +} + +.editable-input { + vertical-align: top; + display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */ + width: auto; /* bootstrap-responsive has width: 100% that breakes layout */ + white-space: normal; /* reset white-space decalred in parent*/ + /* display-inline emulation for IE7*/ + zoom: 1; + *display: inline; +} + +.editable-buttons .editable-cancel { + margin-left: 7px; +} + +/*for jquery-ui buttons need set height to look more pretty*/ +.editable-buttons button.ui-button-icon-only { + height: 24px; + width: 30px; +} + +.editableform-loading { + background: url('../img/loading.gif') center center no-repeat; + height: 25px; + width: auto; + min-width: 25px; +} + +.editable-inline .editableform-loading { + background-position: left 5px; +} + + .editable-error-block { + max-width: 300px; + margin: 5px 0 0 0; + width: auto; + white-space: normal; +} + +/*add padding for jquery ui*/ +.editable-error-block.ui-state-error { + padding: 3px; +} + +.editable-error { + color: red; +} + +/* ---- For specific types ---- */ + +.editableform .editable-date { + padding: 0; + margin: 0; + float: left; +} + +/* move datepicker icon to center of add-on button. See https://github.com/vitalets/x-editable/issues/183 */ +.editable-inline .add-on .icon-th { + margin-top: 3px; + margin-left: 1px; +} + + +/* checklist vertical alignment */ +.editable-checklist label input[type="checkbox"], +.editable-checklist label span { + vertical-align: middle; + margin: 0; +} + +.editable-checklist label { + white-space: nowrap; +} + +/* set exact width of textarea to fit buttons toolbar */ +.editable-wysihtml5 { + width: 566px; + height: 250px; +} + +/* clear button shown as link in date inputs */ +.editable-clear { + clear: both; + font-size: 0.9em; + text-decoration: none; + text-align: right; +} + +/* IOS-style clear button for text inputs */ +.editable-clear-x { + background: url('../img/clear.png') center center no-repeat; + display: block; + width: 13px; + height: 13px; + position: absolute; + opacity: 0.6; + z-index: 100; + + top: 50%; + right: 6px; + margin-top: -6px; + +} + +.editable-clear-x:hover { + opacity: 1; +} + +.editable-pre-wrapped { + white-space: pre-wrap; +} +.editable-container.editable-popup { + max-width: none !important; /* without this rule poshytip/tooltip does not stretch */ +} + +.editable-container.popover { + width: auto; /* without this rule popover does not stretch */ +} + +.editable-container.editable-inline { + display: inline-block; + vertical-align: middle; + width: auto; + /* inline-block emulation for IE7*/ + zoom: 1; + *display: inline; +} + +.editable-container.ui-widget { + font-size: inherit; /* jqueryui widget font 1.1em too big, overwrite it */ + z-index: 9990; /* should be less than select2 dropdown z-index to close dropdown first when click */ +} +.editable-click, +a.editable-click, +a.editable-click:hover { + text-decoration: none; + border-bottom: dashed 1px #0088cc; +} + +.editable-click.editable-disabled, +a.editable-click.editable-disabled, +a.editable-click.editable-disabled:hover { + color: #585858; + cursor: default; + border-bottom: none; +} + +.editable-empty, .editable-empty:hover, .editable-empty:focus{ + font-style: italic; + color: #DD1144; + /* border-bottom: none; */ + text-decoration: none; +} + +.editable-unsaved { + font-weight: bold; +} + +.editable-unsaved:after { +/* content: '*'*/ +} + +.editable-bg-transition { + -webkit-transition: background-color 1400ms ease-out; + -moz-transition: background-color 1400ms ease-out; + -o-transition: background-color 1400ms ease-out; + -ms-transition: background-color 1400ms ease-out; + transition: background-color 1400ms ease-out; +} + +/*see https://github.com/vitalets/x-editable/issues/139 */ +.form-horizontal .editable +{ + padding-top: 5px; + display:inline-block; +} + + +/*! + * Datepicker for Bootstrap + * + * Copyright 2012 Stefan Petre + * Improvements by Andrew Rowls + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + */ +.datepicker { + padding: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + direction: ltr; + /*.dow { + border-top: 1px solid #ddd !important; + }*/ + +} +.datepicker-inline { + width: 220px; +} +.datepicker.datepicker-rtl { + direction: rtl; +} +.datepicker.datepicker-rtl table tr td span { + float: right; +} +.datepicker-dropdown { + top: 0; + left: 0; +} +.datepicker-dropdown:before { + content: ''; + display: inline-block; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-bottom-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: -7px; + left: 6px; +} +.datepicker-dropdown:after { + content: ''; + display: inline-block; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + position: absolute; + top: -6px; + left: 7px; +} +.datepicker > div { + display: none; +} +.datepicker.days div.datepicker-days { + display: block; +} +.datepicker.months div.datepicker-months { + display: block; +} +.datepicker.years div.datepicker-years { + display: block; +} +.datepicker table { + margin: 0; +} +.datepicker td, +.datepicker th { + text-align: center; + width: 20px; + height: 20px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + border: none; +} +.table-striped .datepicker table tr td, +.table-striped .datepicker table tr th { + background-color: transparent; +} +.datepicker table tr td.day:hover { + background: #eeeeee; + cursor: pointer; +} +.datepicker table tr td.old, +.datepicker table tr td.new { + color: #999999; +} +.datepicker table tr td.disabled, +.datepicker table tr td.disabled:hover { + background: none; + color: #999999; + cursor: default; +} +.datepicker table tr td.today, +.datepicker table tr td.today:hover, +.datepicker table tr td.today.disabled, +.datepicker table tr td.today.disabled:hover { + background-color: #fde19a; + background-image: -moz-linear-gradient(top, #fdd49a, #fdf59a); + background-image: -ms-linear-gradient(top, #fdd49a, #fdf59a); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fdd49a), to(#fdf59a)); + background-image: -webkit-linear-gradient(top, #fdd49a, #fdf59a); + background-image: -o-linear-gradient(top, #fdd49a, #fdf59a); + background-image: linear-gradient(top, #fdd49a, #fdf59a); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0); + border-color: #fdf59a #fdf59a #fbed50; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #000; +} +.datepicker table tr td.today:hover, +.datepicker table tr td.today:hover:hover, +.datepicker table tr td.today.disabled:hover, +.datepicker table tr td.today.disabled:hover:hover, +.datepicker table tr td.today:active, +.datepicker table tr td.today:hover:active, +.datepicker table tr td.today.disabled:active, +.datepicker table tr td.today.disabled:hover:active, +.datepicker table tr td.today.active, +.datepicker table tr td.today:hover.active, +.datepicker table tr td.today.disabled.active, +.datepicker table tr td.today.disabled:hover.active, +.datepicker table tr td.today.disabled, +.datepicker table tr td.today:hover.disabled, +.datepicker table tr td.today.disabled.disabled, +.datepicker table tr td.today.disabled:hover.disabled, +.datepicker table tr td.today[disabled], +.datepicker table tr td.today:hover[disabled], +.datepicker table tr td.today.disabled[disabled], +.datepicker table tr td.today.disabled:hover[disabled] { + background-color: #fdf59a; +} +.datepicker table tr td.today:active, +.datepicker table tr td.today:hover:active, +.datepicker table tr td.today.disabled:active, +.datepicker table tr td.today.disabled:hover:active, +.datepicker table tr td.today.active, +.datepicker table tr td.today:hover.active, +.datepicker table tr td.today.disabled.active, +.datepicker table tr td.today.disabled:hover.active { + background-color: #fbf069 \9; +} +.datepicker table tr td.today:hover:hover { + color: #000; +} +.datepicker table tr td.today.active:hover { + color: #fff; +} +.datepicker table tr td.range, +.datepicker table tr td.range:hover, +.datepicker table tr td.range.disabled, +.datepicker table tr td.range.disabled:hover { + background: #eeeeee; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.datepicker table tr td.range.today, +.datepicker table tr td.range.today:hover, +.datepicker table tr td.range.today.disabled, +.datepicker table tr td.range.today.disabled:hover { + background-color: #f3d17a; + background-image: -moz-linear-gradient(top, #f3c17a, #f3e97a); + background-image: -ms-linear-gradient(top, #f3c17a, #f3e97a); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f3c17a), to(#f3e97a)); + background-image: -webkit-linear-gradient(top, #f3c17a, #f3e97a); + background-image: -o-linear-gradient(top, #f3c17a, #f3e97a); + background-image: linear-gradient(top, #f3c17a, #f3e97a); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0); + border-color: #f3e97a #f3e97a #edde34; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.datepicker table tr td.range.today:hover, +.datepicker table tr td.range.today:hover:hover, +.datepicker table tr td.range.today.disabled:hover, +.datepicker table tr td.range.today.disabled:hover:hover, +.datepicker table tr td.range.today:active, +.datepicker table tr td.range.today:hover:active, +.datepicker table tr td.range.today.disabled:active, +.datepicker table tr td.range.today.disabled:hover:active, +.datepicker table tr td.range.today.active, +.datepicker table tr td.range.today:hover.active, +.datepicker table tr td.range.today.disabled.active, +.datepicker table tr td.range.today.disabled:hover.active, +.datepicker table tr td.range.today.disabled, +.datepicker table tr td.range.today:hover.disabled, +.datepicker table tr td.range.today.disabled.disabled, +.datepicker table tr td.range.today.disabled:hover.disabled, +.datepicker table tr td.range.today[disabled], +.datepicker table tr td.range.today:hover[disabled], +.datepicker table tr td.range.today.disabled[disabled], +.datepicker table tr td.range.today.disabled:hover[disabled] { + background-color: #f3e97a; +} +.datepicker table tr td.range.today:active, +.datepicker table tr td.range.today:hover:active, +.datepicker table tr td.range.today.disabled:active, +.datepicker table tr td.range.today.disabled:hover:active, +.datepicker table tr td.range.today.active, +.datepicker table tr td.range.today:hover.active, +.datepicker table tr td.range.today.disabled.active, +.datepicker table tr td.range.today.disabled:hover.active { + background-color: #efe24b \9; +} +.datepicker table tr td.selected, +.datepicker table tr td.selected:hover, +.datepicker table tr td.selected.disabled, +.datepicker table tr td.selected.disabled:hover { + background-color: #9e9e9e; + background-image: -moz-linear-gradient(top, #b3b3b3, #808080); + background-image: -ms-linear-gradient(top, #b3b3b3, #808080); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b3b3b3), to(#808080)); + background-image: -webkit-linear-gradient(top, #b3b3b3, #808080); + background-image: -o-linear-gradient(top, #b3b3b3, #808080); + background-image: linear-gradient(top, #b3b3b3, #808080); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0); + border-color: #808080 #808080 #595959; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.datepicker table tr td.selected:hover, +.datepicker table tr td.selected:hover:hover, +.datepicker table tr td.selected.disabled:hover, +.datepicker table tr td.selected.disabled:hover:hover, +.datepicker table tr td.selected:active, +.datepicker table tr td.selected:hover:active, +.datepicker table tr td.selected.disabled:active, +.datepicker table tr td.selected.disabled:hover:active, +.datepicker table tr td.selected.active, +.datepicker table tr td.selected:hover.active, +.datepicker table tr td.selected.disabled.active, +.datepicker table tr td.selected.disabled:hover.active, +.datepicker table tr td.selected.disabled, +.datepicker table tr td.selected:hover.disabled, +.datepicker table tr td.selected.disabled.disabled, +.datepicker table tr td.selected.disabled:hover.disabled, +.datepicker table tr td.selected[disabled], +.datepicker table tr td.selected:hover[disabled], +.datepicker table tr td.selected.disabled[disabled], +.datepicker table tr td.selected.disabled:hover[disabled] { + background-color: #808080; +} +.datepicker table tr td.selected:active, +.datepicker table tr td.selected:hover:active, +.datepicker table tr td.selected.disabled:active, +.datepicker table tr td.selected.disabled:hover:active, +.datepicker table tr td.selected.active, +.datepicker table tr td.selected:hover.active, +.datepicker table tr td.selected.disabled.active, +.datepicker table tr td.selected.disabled:hover.active { + background-color: #666666 \9; +} +.datepicker table tr td.active, +.datepicker table tr td.active:hover, +.datepicker table tr td.active.disabled, +.datepicker table tr td.active.disabled:hover { + background-color: #006dcc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -ms-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.datepicker table tr td.active:hover, +.datepicker table tr td.active:hover:hover, +.datepicker table tr td.active.disabled:hover, +.datepicker table tr td.active.disabled:hover:hover, +.datepicker table tr td.active:active, +.datepicker table tr td.active:hover:active, +.datepicker table tr td.active.disabled:active, +.datepicker table tr td.active.disabled:hover:active, +.datepicker table tr td.active.active, +.datepicker table tr td.active:hover.active, +.datepicker table tr td.active.disabled.active, +.datepicker table tr td.active.disabled:hover.active, +.datepicker table tr td.active.disabled, +.datepicker table tr td.active:hover.disabled, +.datepicker table tr td.active.disabled.disabled, +.datepicker table tr td.active.disabled:hover.disabled, +.datepicker table tr td.active[disabled], +.datepicker table tr td.active:hover[disabled], +.datepicker table tr td.active.disabled[disabled], +.datepicker table tr td.active.disabled:hover[disabled] { + background-color: #0044cc; +} +.datepicker table tr td.active:active, +.datepicker table tr td.active:hover:active, +.datepicker table tr td.active.disabled:active, +.datepicker table tr td.active.disabled:hover:active, +.datepicker table tr td.active.active, +.datepicker table tr td.active:hover.active, +.datepicker table tr td.active.disabled.active, +.datepicker table tr td.active.disabled:hover.active { + background-color: #003399 \9; +} +.datepicker table tr td span { + display: block; + width: 23%; + height: 54px; + line-height: 54px; + float: left; + margin: 1%; + cursor: pointer; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.datepicker table tr td span:hover { + background: #eeeeee; +} +.datepicker table tr td span.disabled, +.datepicker table tr td span.disabled:hover { + background: none; + color: #999999; + cursor: default; +} +.datepicker table tr td span.active, +.datepicker table tr td span.active:hover, +.datepicker table tr td span.active.disabled, +.datepicker table tr td span.active.disabled:hover { + background-color: #006dcc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -ms-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.datepicker table tr td span.active:hover, +.datepicker table tr td span.active:hover:hover, +.datepicker table tr td span.active.disabled:hover, +.datepicker table tr td span.active.disabled:hover:hover, +.datepicker table tr td span.active:active, +.datepicker table tr td span.active:hover:active, +.datepicker table tr td span.active.disabled:active, +.datepicker table tr td span.active.disabled:hover:active, +.datepicker table tr td span.active.active, +.datepicker table tr td span.active:hover.active, +.datepicker table tr td span.active.disabled.active, +.datepicker table tr td span.active.disabled:hover.active, +.datepicker table tr td span.active.disabled, +.datepicker table tr td span.active:hover.disabled, +.datepicker table tr td span.active.disabled.disabled, +.datepicker table tr td span.active.disabled:hover.disabled, +.datepicker table tr td span.active[disabled], +.datepicker table tr td span.active:hover[disabled], +.datepicker table tr td span.active.disabled[disabled], +.datepicker table tr td span.active.disabled:hover[disabled] { + background-color: #0044cc; +} +.datepicker table tr td span.active:active, +.datepicker table tr td span.active:hover:active, +.datepicker table tr td span.active.disabled:active, +.datepicker table tr td span.active.disabled:hover:active, +.datepicker table tr td span.active.active, +.datepicker table tr td span.active:hover.active, +.datepicker table tr td span.active.disabled.active, +.datepicker table tr td span.active.disabled:hover.active { + background-color: #003399 \9; +} +.datepicker table tr td span.old, +.datepicker table tr td span.new { + color: #999999; +} +.datepicker th.datepicker-switch { + width: 145px; +} +.datepicker thead tr:first-child th, +.datepicker tfoot tr th { + cursor: pointer; +} +.datepicker thead tr:first-child th:hover, +.datepicker tfoot tr th:hover { + background: #eeeeee; +} +.datepicker .cw { + font-size: 10px; + width: 12px; + padding: 0 2px 0 5px; + vertical-align: middle; +} +.datepicker thead tr:first-child th.cw { + cursor: default; + background-color: transparent; +} +.input-append.date .add-on i, +.input-prepend.date .add-on i { + display: block; + cursor: pointer; + width: 16px; + height: 16px; +} +.input-daterange input { + text-align: center; +} +.input-daterange input:first-child { + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.input-daterange input:last-child { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.input-daterange .add-on { + display: inline-block; + width: auto; + min-width: 16px; + height: 18px; + padding: 4px 5px; + font-weight: normal; + line-height: 18px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + vertical-align: middle; + background-color: #eeeeee; + border: 1px solid #ccc; + margin-left: -5px; + margin-right: -5px; +} diff --git a/cps/static/css/libs/bootstrap-table.min.css b/cps/static/css/libs/bootstrap-table.min.css new file mode 100644 index 00000000..770b6728 --- /dev/null +++ b/cps/static/css/libs/bootstrap-table.min.css @@ -0,0 +1 @@ +.fixed-table-container .bs-checkbox,.fixed-table-container .no-records-found{text-align:center}.fixed-table-body thead th .th-inner,.table td,.table th{box-sizing:border-box}.bootstrap-table .table{margin-bottom:0!important;border-bottom:1px solid #ddd;border-collapse:collapse!important;border-radius:1px}.bootstrap-table .table:not(.table-condensed),.bootstrap-table .table:not(.table-condensed)>tbody>tr>td,.bootstrap-table .table:not(.table-condensed)>tbody>tr>th,.bootstrap-table .table:not(.table-condensed)>tfoot>tr>td,.bootstrap-table .table:not(.table-condensed)>tfoot>tr>th,.bootstrap-table .table:not(.table-condensed)>thead>tr>td{padding:8px}.bootstrap-table .table.table-no-bordered>tbody>tr>td,.bootstrap-table .table.table-no-bordered>thead>tr>th{border-right:2px solid transparent}.bootstrap-table .table.table-no-bordered>tbody>tr>td:last-child{border-right:none}.fixed-table-container{position:relative;clear:both;border:1px solid #ddd;border-radius:4px;-webkit-border-radius:4px;-moz-border-radius:4px}.fixed-table-container.table-no-bordered{border:1px solid transparent}.fixed-table-footer,.fixed-table-header{overflow:hidden}.fixed-table-footer{border-top:1px solid #ddd}.fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.fixed-table-container table{width:100%}.fixed-table-container thead th{height:0;padding:0;margin:0;border-left:1px solid #ddd}.fixed-table-container thead th:focus{outline:transparent solid 0}.fixed-table-container thead th:first-child:not([data-not-first-th]){border-left:none;border-top-left-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px}.fixed-table-container tbody td .th-inner,.fixed-table-container thead th .th-inner{padding:8px;line-height:24px;vertical-align:top;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fixed-table-container thead th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px}.fixed-table-container thead th .both{background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC')}.fixed-table-container thead th .asc{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==)}.fixed-table-container thead th .desc{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII=)}.fixed-table-container th.detail{width:30px}.fixed-table-container tbody td{border-left:1px solid #ddd}.fixed-table-container tbody tr:first-child td{border-top:none}.fixed-table-container tbody td:first-child{border-left:none}.fixed-table-container tbody .selected td{background-color:#f5f5f5}.fixed-table-container input[type=radio],.fixed-table-container input[type=checkbox]{margin:0 auto!important}.fixed-table-pagination .pagination-detail,.fixed-table-pagination div.pagination{margin-top:10px;margin-bottom:10px}.fixed-table-pagination div.pagination .pagination{margin:0}.fixed-table-pagination .pagination a{padding:6px 12px;line-height:1.428571429}.fixed-table-pagination .pagination-info{line-height:34px;margin-right:5px}.fixed-table-pagination .btn-group{position:relative;display:inline-block;vertical-align:middle}.fixed-table-pagination .dropup .dropdown-menu{margin-bottom:0}.fixed-table-pagination .page-list{display:inline-block}.fixed-table-toolbar .columns-left{margin-right:5px}.fixed-table-toolbar .columns-right{margin-left:5px}.fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.428571429}.fixed-table-toolbar .bs-bars,.fixed-table-toolbar .columns,.fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px;line-height:34px}.fixed-table-pagination li.disabled a{pointer-events:none;cursor:default}.fixed-table-loading{display:none;position:absolute;top:42px;right:0;bottom:0;left:0;z-index:99;background-color:#fff;text-align:center}.fixed-table-body .card-view .title{font-weight:700;display:inline-block;min-width:30%;text-align:left!important}.table td,.table th{vertical-align:middle}.fixed-table-toolbar .dropdown-menu{text-align:left;max-height:300px;overflow:auto}.fixed-table-toolbar .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.fixed-table-toolbar .btn-group>.btn-group>.btn{border-radius:0}.fixed-table-toolbar .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.fixed-table-toolbar .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .table>thead>tr>th{vertical-align:bottom;border-bottom:1px solid #ddd}.bootstrap-table .table thead>tr>th{padding:0;margin:0}.bootstrap-table .fixed-table-footer tbody>tr>td{padding:0!important}.bootstrap-table .fixed-table-footer .table{border-bottom:none;border-radius:0;padding:0!important}.bootstrap-table .pull-right .dropdown-menu{right:0;left:auto}p.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}.fixed-table-pagination:after,.fixed-table-toolbar:after{content:"";display:block;clear:both}.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%!important;background:#FFF} \ No newline at end of file diff --git a/cps/static/css/style.css b/cps/static/css/style.css index a89291e5..16c3cb36 100644 --- a/cps/static/css/style.css +++ b/cps/static/css/style.css @@ -48,7 +48,7 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te .btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open .dropdown-toggle.btn-primary{ background-color: #1C5484; } .btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { background-color: #89B9E2; } .btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left:0px; } - +.panel-body {background-color: #f5f5f5;} .spinner {margin:0 41%;} .spinner2 {margin:0 41%;} @@ -94,5 +94,13 @@ input.pill:not(:checked) + label .glyphicon { .upload-format-input-text {display: initial;} #btn-upload-format {display: none;} -.upload-format-input-text {display: initial;} -#btn-upload-format {display: none;} +.upload-cover-input-text {display: initial;} +#btn-upload-cover {display: none;} + +.panel-title > a { text-decoration: none;} + +.editable-buttons { display:inline-block; margin-left: 7px ;} +.editable-input { display:inline-block;} +.editable-cancel { margin-bottom: 0px !important; margin-left: 7px !important;} +.editable-submit { margin-bottom: 0px !important;} + diff --git a/cps/static/favicon.ico b/cps/static/favicon.ico index 46339365..0774d0f9 100644 Binary files a/cps/static/favicon.ico and b/cps/static/favicon.ico differ diff --git a/cps/static/img/.gitignore b/cps/static/img/.gitignore deleted file mode 100644 index e69de29b..00000000 diff --git a/cps/static/js/archive.js b/cps/static/js/archive.js index 28aae182..b859513f 100644 --- a/cps/static/js/archive.js +++ b/cps/static/js/archive.js @@ -143,13 +143,12 @@ bitjs.archive = bitjs.archive || {}; * Progress event. */ bitjs.archive.UnarchiveProgressEvent = function( - currentFilename, - currentFileNumber, - currentBytesUnarchivedInFile, - currentBytesUnarchived, - totalUncompressedBytesInArchive, - totalFilesInArchive) - { + currentFilename, + currentFileNumber, + currentBytesUnarchivedInFile, + currentBytesUnarchived, + totalUncompressedBytesInArchive, + totalFilesInArchive) { bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.PROGRESS); this.currentFilename = currentFilename; diff --git a/cps/static/js/bytestream.js b/cps/static/js/bytestream.js new file mode 100644 index 00000000..cb5df363 --- /dev/null +++ b/cps/static/js/bytestream.js @@ -0,0 +1,308 @@ +/* + * bytestream.js + * + * Provides readers for byte streams. + * + * Licensed under the MIT License + * + * Copyright(c) 2011 Google Inc. + * Copyright(c) 2011 antimatter15 + */ + +var bitjs = bitjs || {}; +bitjs.io = bitjs.io || {}; + + +/** + * This object allows you to peek and consume bytes as numbers and strings out + * of a stream. More bytes can be pushed into the back of the stream via the + * push() method. + */ +bitjs.io.ByteStream = class { + /** + * @param {ArrayBuffer} ab The ArrayBuffer object. + * @param {number=} opt_offset The offset into the ArrayBuffer + * @param {number=} opt_length The length of this BitStream + */ + constructor(ab, opt_offset, opt_length) { + if (!(ab instanceof ArrayBuffer)) { + throw 'Error! BitArray constructed with an invalid ArrayBuffer object'; + } + + const offset = opt_offset || 0; + const length = opt_length || ab.byteLength; + + /** + * The current page of bytes in the stream. + * @type {Uint8Array} + * @private + */ + this.bytes = new Uint8Array(ab, offset, length); + + /** + * The next pages of bytes in the stream. + * @type {Array} + * @private + */ + this.pages_ = []; + + /** + * The byte in the current page that we will read next. + * @type {Number} + * @private + */ + this.ptr = 0; + + /** + * An ever-increasing number. + * @type {Number} + * @private + */ + this.bytesRead_ = 0; + } + + /** + * Returns how many bytes have been read in the stream since the beginning of time. + */ + getNumBytesRead() { + return this.bytesRead_; + } + + /** + * Returns how many bytes are currently in the stream left to be read. + */ + getNumBytesLeft() { + const bytesInCurrentPage = (this.bytes.byteLength - this.ptr); + return this.pages_.reduce((acc, arr) => acc + arr.length, bytesInCurrentPage); + } + + /** + * Move the pointer ahead n bytes. If the pointer is at the end of the current array + * of bytes and we have another page of bytes, point at the new page. This is a private + * method, no validation is done. + * @param {number} n Number of bytes to increment. + * @private + */ + movePointer_(n) { + this.ptr += n; + this.bytesRead_ += n; + while (this.ptr >= this.bytes.length && this.pages_.length > 0) { + this.ptr -= this.bytes.length; + this.bytes = this.pages_.shift(); + } + } + + /** + * Peeks at the next n bytes as an unsigned number but does not advance the + * pointer. + * @param {number} n The number of bytes to peek at. Must be a positive integer. + * @return {number} The n bytes interpreted as an unsigned number. + */ + peekNumber(n) { + const num = parseInt(n, 10); + if (n !== num || num < 0) { + throw 'Error! Called peekNumber() with a non-positive integer'; + } else if (num === 0) { + return 0; + } + + if (n > 4) { + throw 'Error! Called peekNumber(' + n + + ') but this method can only reliably read numbers up to 4 bytes long'; + } + + if (this.getNumBytesLeft() < num) { + throw 'Error! Overflowed the byte stream while peekNumber()! n=' + num + + ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); + } + + let result = 0; + let curPage = this.bytes; + let pageIndex = 0; + let ptr = this.ptr; + for (let i = 0; i < num; ++i) { + result |= (curPage[ptr++] << (i * 8)); + + if (ptr >= curPage.length) { + curPage = this.pages_[pageIndex++]; + ptr = 0; + } + } + + return result; + } + + + /** + * Returns the next n bytes as an unsigned number (or -1 on error) + * and advances the stream pointer n bytes. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @return {number} The n bytes interpreted as an unsigned number. + */ + readNumber(n) { + const num = this.peekNumber(n); + this.movePointer_(n); + return num; + } + + + /** + * Returns the next n bytes as a signed number but does not advance the + * pointer. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @return {number} The bytes interpreted as a signed number. + */ + peekSignedNumber(n) { + let num = this.peekNumber(n); + const HALF = Math.pow(2, (n * 8) - 1); + const FULL = HALF * 2; + + if (num >= HALF) num -= FULL; + + return num; + } + + + /** + * Returns the next n bytes as a signed number and advances the stream pointer. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @return {number} The bytes interpreted as a signed number. + */ + readSignedNumber(n) { + const num = this.peekSignedNumber(n); + this.movePointer_(n); + return num; + } + + + /** + * This returns n bytes as a sub-array, advancing the pointer if movePointers + * is true. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @param {boolean} movePointers Whether to move the pointers. + * @return {Uint8Array} The subarray. + */ + peekBytes(n, movePointers) { + const num = parseInt(n, 10); + if (n !== num || num < 0) { + throw 'Error! Called peekBytes() with a non-positive integer'; + } else if (num === 0) { + return new Uint8Array(); + } + + const totalBytesLeft = this.getNumBytesLeft(); + if (num > totalBytesLeft) { + throw 'Error! Overflowed the byte stream during peekBytes! n=' + num + + ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); + } + + const result = new Uint8Array(num); + let curPage = this.bytes; + let ptr = this.ptr; + let bytesLeftToCopy = num; + let pageIndex = 0; + while (bytesLeftToCopy > 0) { + const bytesLeftInPage = curPage.length - ptr; + const sourceLength = Math.min(bytesLeftToCopy, bytesLeftInPage); + + result.set(curPage.subarray(ptr, ptr + sourceLength), num - bytesLeftToCopy); + + ptr += sourceLength; + if (ptr >= curPage.length) { + curPage = this.pages_[pageIndex++]; + ptr = 0; + } + + bytesLeftToCopy -= sourceLength; + } + + if (movePointers) { + this.movePointer_(num); + } + + return result; + } + + /** + * Reads the next n bytes as a sub-array. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @return {Uint8Array} The subarray. + */ + readBytes(n) { + return this.peekBytes(n, true); + } + + /** + * Peeks at the next n bytes as an ASCII string but does not advance the pointer. + * @param {number} n The number of bytes to peek at. Must be a positive integer. + * @return {string} The next n bytes as a string. + */ + peekString(n) { + const num = parseInt(n, 10); + if (n !== num || num < 0) { + throw 'Error! Called peekString() with a non-positive integer'; + } else if (num === 0) { + return ''; + } + + const totalBytesLeft = this.getNumBytesLeft(); + if (num > totalBytesLeft) { + throw 'Error! Overflowed the byte stream while peekString()! n=' + num + + ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); + } + + let result = new Array(num); + let curPage = this.bytes; + let pageIndex = 0; + let ptr = this.ptr; + for (let i = 0; i < num; ++i) { + result[i] = String.fromCharCode(curPage[ptr++]); + if (ptr >= curPage.length) { + curPage = this.pages_[pageIndex++]; + ptr = 0; + } + } + + return result.join(''); + } + + /** + * Returns the next n bytes as an ASCII string and advances the stream pointer + * n bytes. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @return {string} The next n bytes as a string. + */ + readString(n) { + const strToReturn = this.peekString(n); + this.movePointer_(n); + return strToReturn; + } + + /** + * Feeds more bytes into the back of the stream. + * @param {ArrayBuffer} ab + */ + push(ab) { + if (!(ab instanceof ArrayBuffer)) { + throw 'Error! ByteStream.push() called with an invalid ArrayBuffer object'; + } + + this.pages_.push(new Uint8Array(ab)); + // If the pointer is at the end of the current page of bytes, this will advance + // to the next page. + this.movePointer_(0); + } + + /** + * Creates a new ByteStream from this ByteStream that can be read / peeked. + * @return {bitjs.io.ByteStream} A clone of this ByteStream. + */ + tee() { + const clone = new bitjs.io.ByteStream(this.bytes.buffer); + clone.bytes = this.bytes; + clone.ptr = this.ptr; + clone.pages_ = this.pages_.slice(); + clone.bytesRead_ = this.bytesRead_; + return clone; + } +} diff --git a/cps/static/js/edit_books.js b/cps/static/js/edit_books.js index 3932e3b6..1d182887 100644 --- a/cps/static/js/edit_books.js +++ b/cps/static/js/edit_books.js @@ -29,6 +29,40 @@ if ($("#description").length) { } } +if (!Modernizr.inputtypes.date) { + $("#Publishstart").datepicker({ + format: "yyyy-mm-dd", + language: language + }).on("change", function () { + // Show localized date over top of the standard YYYY-MM-DD date + var pubDate; + var results = /(\d{4})[-\/\\](\d{1,2})[-\/\\](\d{1,2})/.exec(this.value); // YYYY-MM-DD + if (results) { + pubDate = new Date(results[1], parseInt(results[2], 10) - 1, results[3]) || new Date(this.value); + $("#fake_Publishstart") + .val(pubDate.toLocaleDateString(language)) + .removeClass("hidden"); + } + }).trigger("change"); +} + +if (!Modernizr.inputtypes.date) { + $("#Publishend").datepicker({ + format: "yyyy-mm-dd", + language: language + }).on("change", function () { + // Show localized date over top of the standard YYYY-MM-DD date + var pubDate; + var results = /(\d{4})[-\/\\](\d{1,2})[-\/\\](\d{1,2})/.exec(this.value); // YYYY-MM-DD + if (results) { + pubDate = new Date(results[1], parseInt(results[2], 10) - 1, results[3]) || new Date(this.value); + $("#fake_Publishend") + .val(pubDate.toLocaleDateString(language)) + .removeClass("hidden"); + } + }).trigger("change"); +} + /* Takes a prefix, query typeahead callback, Bloodhound typeahead adapter and returns the completions it gets from the bloodhound engine prefixed. @@ -46,8 +80,7 @@ function prefixedSource(prefix, query, cb, bhAdapter) { function getPath() { var jsFileLocation = $("script[src*=edit_books]").attr("src"); // the js file path - jsFileLocation = jsFileLocation.replace("/static/js/edit_books.js", ""); // the js folder path - return jsFileLocation; + return jsFileLocation.substr(0, jsFileLocation.search("/static/js/edit_books.js")); // the js folder path } var authors = new Bloodhound({ @@ -213,3 +246,12 @@ $("#btn-upload-format").on("change", function () { } // Remove c:\fake at beginning from localhost chrome $("#upload-format").html(filename); }); + +$("#btn-upload-cover").on("change", function () { + var filename = $(this).val(); + if (filename.substring(3, 11) === "fakepath") { + filename = filename.substring(12); + } // Remove c:\fake at beginning from localhost chrome + $("#upload-cover").html(filename); +}); + diff --git a/cps/static/js/io.js b/cps/static/js/io.js index 6cc4d81c..292f5f95 100644 --- a/cps/static/js/io.js +++ b/cps/static/js/io.js @@ -121,7 +121,7 @@ bitjs.io = bitjs.io || {}; * @return {number} The peeked bits, as an unsigned number. */ bitjs.io.BitStream.prototype.peekBitsRtl = function(n, movePointers) { - if (n <= 0 || typeof n != typeof 1) { + if (n <= 0 || typeof n !== typeof 1) { return 0; } @@ -150,8 +150,7 @@ bitjs.io = bitjs.io || {}; bytePtr++; bitPtr = 0; n -= numBitsLeftInThisByte; - } - else { + } else { result <<= n; result |= ((bytes[bytePtr] & (BITMASK[n] << (8 - n - bitPtr))) >> (8 - n - bitPtr)); diff --git a/cps/static/js/kthoom.js b/cps/static/js/kthoom.js index 17ee8097..6a107b9a 100644 --- a/cps/static/js/kthoom.js +++ b/cps/static/js/kthoom.js @@ -15,7 +15,7 @@ * Typed Arrays: http://www.khronos.org/registry/typedarray/specs/latest/#6 */ -/* global bitjs */ +/* global screenfull, bitjs */ if (window.opera) { window.console.log = function(str) { @@ -35,67 +35,79 @@ function getElem(id) { return document.getElementById(id); } -if (window.kthoom === undefined) { +if (typeof window.kthoom === "undefined" ) { kthoom = {}; } // key codes kthoom.Key = { ESCAPE: 27, + SPACE: 32, LEFT: 37, UP: 38, RIGHT: 39, - DOWN: 40, - A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71, H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, + DOWN: 40, + A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71, H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, N: 78, O: 79, P: 80, Q: 81, R: 82, S: 83, T: 84, U: 85, V: 86, W: 87, X: 88, Y: 89, Z: 90, QUESTION_MARK: 191, LEFT_SQUARE_BRACKET: 219, RIGHT_SQUARE_BRACKET: 221 }; -// The rotation orientation of the comic. -kthoom.rotateTimes = 0; - // global variables var unarchiver = null; var currentImage = 0; var imageFiles = []; var imageFilenames = []; var totalImages = 0; -var lastCompletion = 0; -var hflip = false, vflip = false, fitMode = kthoom.Key.B; -var canKeyNext = true, canKeyPrev = true; +var settings = { + hflip: false, + vflip: false, + rotateTimes: 0, + fitMode: kthoom.Key.B, + theme: "light" +}; kthoom.saveSettings = function() { - localStorage.kthoomSettings = JSON.stringify({ - rotateTimes: kthoom.rotateTimes, - hflip: hflip, - vflip: vflip, - fitMode: fitMode - }); + localStorage.kthoomSettings = JSON.stringify(settings); }; kthoom.loadSettings = function() { try { - if (localStorage.kthoomSettings.length < 10){ + if (!localStorage.kthoomSettings) { return; } - var s = JSON.parse(localStorage.kthoomSettings); - kthoom.rotateTimes = s.rotateTimes; - hflip = s.hflip; - vflip = s.vflip; - fitMode = s.fitMode; + + $.extend(settings, JSON.parse(localStorage.kthoomSettings)); + + kthoom.setSettings(); } catch (err) { alert("Error load settings"); } }; +kthoom.setSettings = function() { + // Set settings control values + $.each(settings, function(key, value) { + if (typeof value === "boolean") { + $("input[name=" + key + "]").prop("checked", value); + } else { + $("input[name=" + key + "]").val([value]); + } + }); +}; + var createURLFromArray = function(array, mimeType) { var offset = array.byteOffset, 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. @@ -122,152 +134,24 @@ var createURLFromArray = function(array, mimeType) { // Stores an image filename and its data: URI. -// TODO: investigate if we really need to store as base64 (leave off ;base64 and just -// non-safe URL characters are encoded as %xx ?) -// This would save 25% on memory since base64-encoded strings are 4/3 the size of the binary kthoom.ImageFile = function(file) { this.filename = file.filename; var fileExtension = file.filename.split(".").pop().toLowerCase(); var mimeType = fileExtension === "png" ? "image/png" : (fileExtension === "jpg" || fileExtension === "jpeg") ? "image/jpeg" : - fileExtension === "gif" ? "image/gif" : null; + fileExtension === "gif" ? "image/gif" : fileExtension == 'svg' ? 'image/xml+svg' : undefined; this.dataURI = createURLFromArray(file.fileData, mimeType); this.data = file; }; -kthoom.initProgressMeter = function() { - var svgns = "http://www.w3.org/2000/svg"; - var pdiv = $("#progress")[0]; - var svg = document.createElementNS(svgns, "svg"); - svg.style.width = "100%"; - svg.style.height = "100%"; - - var defs = document.createElementNS(svgns, "defs"); - - var patt = document.createElementNS(svgns, "pattern"); - patt.id = "progress_pattern"; - patt.setAttribute("width", "30"); - patt.setAttribute("height", "20"); - patt.setAttribute("patternUnits", "userSpaceOnUse"); - - var rect = document.createElementNS(svgns, "rect"); - rect.setAttribute("width", "100%"); - rect.setAttribute("height", "100%"); - rect.setAttribute("fill", "#cc2929"); - - var poly = document.createElementNS(svgns, "polygon"); - poly.setAttribute("fill", "yellow"); - poly.setAttribute("points", "15,0 30,0 15,20 0,20"); - - patt.appendChild(rect); - patt.appendChild(poly); - defs.appendChild(patt); - - svg.appendChild(defs); - - var g = document.createElementNS(svgns, "g"); - - var outline = document.createElementNS(svgns, "rect"); - outline.setAttribute("y", "1"); - outline.setAttribute("width", "100%"); - outline.setAttribute("height", "15"); - outline.setAttribute("fill", "#777"); - outline.setAttribute("stroke", "white"); - outline.setAttribute("rx", "5"); - outline.setAttribute("ry", "5"); - g.appendChild(outline); - - var title = document.createElementNS(svgns, "text"); - title.id = "progress_title"; - title.appendChild(document.createTextNode("0%")); - title.setAttribute("y", "13"); - title.setAttribute("x", "99.5%"); - title.setAttribute("fill", "white"); - title.setAttribute("font-size", "12px"); - title.setAttribute("text-anchor", "end"); - g.appendChild(title); - - var meter = document.createElementNS(svgns, "rect"); - meter.id = "meter"; - meter.setAttribute("width", "0%"); - meter.setAttribute("height", "17"); - meter.setAttribute("fill", "url(#progress_pattern)"); - meter.setAttribute("rx", "5"); - meter.setAttribute("ry", "5"); - - var meter2 = document.createElementNS(svgns, "rect"); - meter2.id = "meter2"; - meter2.setAttribute("width", "0%"); - meter2.setAttribute("height", "17"); - meter2.setAttribute("opacity", "0.8"); - meter2.setAttribute("fill", "#007fff"); - meter2.setAttribute("rx", "5"); - meter2.setAttribute("ry", "5"); - - g.appendChild(meter); - g.appendChild(meter2); - - var page = document.createElementNS(svgns, "text"); - page.id = "page"; - page.appendChild(document.createTextNode("0/0")); - page.setAttribute("y", "13"); - page.setAttribute("x", "0.5%"); - page.setAttribute("fill", "white"); - page.setAttribute("font-size", "12px"); - g.appendChild(page); - - - svg.appendChild(g); - pdiv.appendChild(svg); - var l; - svg.onclick = function(e) { - for (var x = pdiv, l = 0; x !== document.documentElement; x = x.parentNode) l += x.offsetLeft; - var page = Math.max(1, Math.ceil(((e.clientX - l) / pdiv.offsetWidth) * totalImages)) - 1; +function initProgressClick() { + $("#progress").click(function(e) { + var page = Math.max(1, Math.ceil((e.offsetX / $(this).width()) * totalImages)) - 1; currentImage = page; updatePage(); - }; -} - -kthoom.setProgressMeter = function(pct, optLabel) { - pct = (pct * 100); - var part = 1 / totalImages; - var remain = ((pct - lastCompletion) / 100) / part; - var fract = Math.min(1, remain); - var smartpct = ((imageFiles.length / totalImages) + (fract * part)) * 100; - if (totalImages === 0) smartpct = pct; - - // + Math.min((pct - lastCompletion), 100/totalImages * 0.9 + (pct - lastCompletion - 100/totalImages)/2, 100/totalImages); - var oldval = parseFloat(getElem("meter").getAttribute("width")); - if (isNaN(oldval)) oldval = 0; - var weight = 0.5; - smartpct = ((weight * smartpct) + ((1 - weight) * oldval)); - if (pct === 100) smartpct = 100; - - if (!isNaN(smartpct)) { - getElem("meter").setAttribute("width", smartpct + "%"); - } - var title = getElem("progress_title"); - while (title.firstChild) title.removeChild(title.firstChild); - - var labelText = pct.toFixed(2) + "% " + imageFiles.length + "/" + totalImages + ""; - if (optLabel) { - labelText = optLabel + " " + labelText; - } - title.appendChild(document.createTextNode(labelText)); - - getElem("meter2").setAttribute("width", - 100 * (totalImages === 0 ? 0 : ((currentImage + 1) / totalImages)) + "%"); - - var titlePage = getElem("page"); - while (titlePage.firstChild) titlePage.removeChild(titlePage.firstChild); - titlePage.appendChild(document.createTextNode( (currentImage + 1) + "/" + totalImages )); - - if (pct > 0) { - //getElem('nav').className = ''; - getElem("progress").className = ""; - } -} + }); +}; function loadFromArrayBuffer(ab) { var start = (new Date).getTime(); @@ -286,8 +170,7 @@ function loadFromArrayBuffer(ab) { function(e) { var percentage = e.currentBytesUnarchived / e.totalUncompressedBytesInArchive; totalImages = e.totalFilesInArchive; - kthoom.setProgressMeter(percentage, "Unzipping"); - // display nav + updateProgress(percentage *100); lastCompletion = percentage * 100; }); unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.EXTRACT, @@ -299,11 +182,20 @@ function loadFromArrayBuffer(ab) { if (imageFilenames.indexOf(f.filename) === -1) { imageFilenames.push(f.filename); imageFiles.push(new kthoom.ImageFile(f)); + // add thumbnails to the TOC list + $("#thumbnails").append( + "
  • " + + "" + + "" + + "" + imageFiles.length + "" + + "" + + "
  • " + ); } } // display first page if we haven't yet if (imageFiles.length === currentImage + 1) { - updatePage(); + updatePage(lastCompletion); } }); unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.FINISH, @@ -317,19 +209,54 @@ function loadFromArrayBuffer(ab) { } } +function scrollTocToActive() { + // Scroll to the thumbnail in the TOC on page change + $('#tocView').stop().animate({ + scrollTop: $('#tocView a.active').position().top + }, 200); +} function updatePage() { - var title = getElem("page"); - while (title.firstChild) title.removeChild(title.firstChild); - title.appendChild(document.createTextNode( (currentImage + 1 ) + "/" + totalImages )); + $('.page').text((currentImage + 1 ) + "/" + totalImages); + + // Mark the current page in the TOC + $('#tocView a[data-page]') + // Remove the currently active thumbnail + .removeClass('active') + // Find the new one + .filter('[data-page='+ (currentImage + 1) +']') + // Set it to active + .addClass('active'); + + scrollTocToActive(); + updateProgress(); - getElem("meter2").setAttribute("width", - 100 * (totalImages === 0 ? 0 : ((currentImage + 1 ) / totalImages)) + "%"); if (imageFiles[currentImage]) { setImage(imageFiles[currentImage].dataURI); } else { setImage("loading"); } + + $("body").toggleClass("dark-theme", settings.theme === "dark"); + + kthoom.setSettings(); + kthoom.saveSettings(); +} + +function updateProgress(loadPercentage) { + // Set the load/unzip progress if it's passed in + if (loadPercentage) { + $("#progress .bar-load").css({ width: loadPercentage + "%" }); + + if (loadPercentage === 100) { + $("#progress") + .removeClass('loading') + .find(".load").text(''); + } + } + + // Set page progress bar + $("#progress .bar-read").css({ width: totalImages === 0 ? 0 : Math.round((currentImage + 1) / totalImages * 100) + "%"}); } function setImage(url) { @@ -340,81 +267,92 @@ function setImage(url) { updateScale(true); canvas.width = innerWidth - 100; canvas.height = 200; - x.fillStyle = "red"; - x.font = "50px sans-serif"; + x.fillStyle = "black"; + x.textAlign = "center"; + x.font = "24px sans-serif"; x.strokeStyle = "black"; - x.fillText("Loading Page #" + (currentImage + 1), 100, 100); + x.fillText("Loading Page #" + (currentImage + 1), innerWidth / 2, 100); } else { - if ($("body").css("scrollHeight") / innerHeight > 1) { - $("body").css("overflowY", "scroll"); - } - - var img = new Image(); - img.onerror = function() { - canvas.width = innerWidth - 100; - canvas.height = 300; + if (url === "error") { updateScale(true); - x.fillStyle = "orange"; - x.font = "50px sans-serif"; + canvas.width = innerWidth - 100; + canvas.height = 200; + x.fillStyle = "black"; + x.textAlign = "center"; + x.font = "24px sans-serif"; x.strokeStyle = "black"; - x.fillText("Page #" + (currentImage + 1) + " (" + - imageFiles[currentImage].filename + ")", 100, 100); - x.fillStyle = "red"; - x.fillText("Is corrupt or not an image", 100, 200); - - var xhr = new XMLHttpRequest(); - if (/(html|htm)$/.test(imageFiles[currentImage].filename)) { - xhr.open("GET", url, true); - xhr.onload = function() { - //document.getElementById('mainText').style.display = ''; - $("#mainText").css("display", ""); - $("#mainText").innerHTML(""); - } - xhr.send(null); - } else if (!/(jpg|jpeg|png|gif)$/.test(imageFiles[currentImage].filename) && imageFiles[currentImage].data.uncompressedSize < 10 * 1024) { - xhr.open("GET", url, true); - xhr.onload = function() { - $("#mainText").css("display", ""); - $("#mainText").innerText(xhr.responseText); - }; - xhr.send(null); - } - }; - img.onload = function() { - var h = img.height, - w = img.width, - sw = w, - sh = h; - kthoom.rotateTimes = (4 + kthoom.rotateTimes) % 4; - x.save(); - if (kthoom.rotateTimes % 2 === 1) { - sh = w; - sw = h; - } - canvas.height = sh; - canvas.width = sw; - x.translate(sw / 2, sh / 2); - x.rotate(Math.PI / 2 * kthoom.rotateTimes); - x.translate(-w / 2, -h / 2); - if (vflip) { - x.scale(1, -1); - x.translate(0, -h); + x.fillText("Unable to decompress image #" + (currentImage + 1), innerWidth / 2, 100); + } else { + if ($("body").css("scrollHeight") / innerHeight > 1) { + $("body").css("overflowY", "scroll"); } - if (hflip) { - x.scale(-1, 1); - x.translate(-w, 0); - } - canvas.style.display = "none"; - scrollTo(0, 0); - x.drawImage(img, 0, 0); - updateScale(); + var img = new Image(); + img.onerror = function() { + canvas.width = innerWidth - 100; + canvas.height = 300; + updateScale(true); + x.fillStyle = "black"; + x.font = "50px sans-serif"; + x.strokeStyle = "black"; + x.fillText("Page #" + (currentImage + 1) + " (" + + imageFiles[currentImage].filename + ")", innerWidth / 2, 100); + x.fillStyle = "black"; + x.fillText("Is corrupt or not an image", innerWidth / 2, 200); + + var xhr = new XMLHttpRequest(); + if (/(html|htm)$/.test(imageFiles[currentImage].filename)) { + xhr.open("GET", url, true); + xhr.onload = function() { + $("#mainText").css("display", ""); + $("#mainText").innerHTML(""); + } + xhr.send(null); + } else if (!/(jpg|jpeg|png|gif)$/.test(imageFiles[currentImage].filename) && imageFiles[currentImage].data.uncompressedSize < 10 * 1024) { + xhr.open("GET", url, true); + xhr.onload = function() { + $("#mainText").css("display", ""); + $("#mainText").innerText(xhr.responseText); + }; + xhr.send(null); + } + }; + img.onload = function() { + var h = img.height, + w = img.width, + sw = w, + sh = h; + settings.rotateTimes = (4 + settings.rotateTimes) % 4; + x.save(); + if (settings.rotateTimes % 2 === 1) { + sh = w; + sw = h; + } + canvas.height = sh; + canvas.width = sw; + x.translate(sw / 2, sh / 2); + x.rotate(Math.PI / 2 * settings.rotateTimes); + x.translate(-w / 2, -h / 2); + if (settings.vflip) { + x.scale(1, -1); + x.translate(0, -h); + } + if (settings.hflip) { + x.scale(-1, 1); + x.translate(-w, 0); + } + canvas.style.display = "none"; + scrollTo(0, 0); + x.drawImage(img, 0, 0); + + updateScale(false); - canvas.style.display = ""; - $("body").css("overflowY", ""); - x.restore(); - }; - img.src = url; + canvas.style.display = ""; + $("body").css("overflowY", ""); + x.restore(); + }; + img.src = url; + } } } @@ -444,149 +382,254 @@ function updateScale(clear) { mainImageStyle.height = ""; mainImageStyle.maxWidth = ""; mainImageStyle.maxHeight = ""; - var maxheight = innerHeight - 15; - if (!/main/.test(getElem("titlebar").className)) { - maxheight -= 25; - } - if (clear || fitMode === kthoom.Key.N) { - } else if (fitMode === kthoom.Key.B) { - mainImageStyle.maxWidth = "100%"; - mainImageStyle.maxHeight = maxheight + "px"; - } else if (fitMode === kthoom.Key.H) { - mainImageStyle.height = maxheight + "px"; - } else if (fitMode === kthoom.Key.W) { - mainImageStyle.width = "100%"; + var maxheight = innerHeight - 50; + + if (!clear) { + switch (settings.fitMode) { + case kthoom.Key.B: + mainImageStyle.maxWidth = "100%"; + mainImageStyle.maxHeight = maxheight + "px"; + break; + case kthoom.Key.H: + mainImageStyle.height = maxheight + "px"; + break; + case kthoom.Key.W: + mainImageStyle.width = "100%"; + break; + default: + break; + } } + $("#mainContent").css({maxHeight: maxheight + 5}); + kthoom.setSettings(); kthoom.saveSettings(); } function keyHandler(evt) { - var code = evt.keyCode; - - if ($("#progress").css("display") === "none"){ - return; - } - canKeyNext = (($("body").css("offsetWidth") + $("body").css("scrollLeft")) / $("body").css("scrollWidth")) >= 1; - canKeyPrev = (scrollX <= 0); - - if (evt.ctrlKey || evt.shiftKey || evt.metaKey) return; - switch (code) { + var hasModifier = evt.ctrlKey || evt.shiftKey || evt.metaKey; + switch (evt.keyCode) { case kthoom.Key.LEFT: - if (canKeyPrev) showPrevPage(); + if (hasModifier) break; + showPrevPage(); break; case kthoom.Key.RIGHT: - if (canKeyNext) showNextPage(); + if (hasModifier) break; + showNextPage(); break; case kthoom.Key.L: - kthoom.rotateTimes--; - if (kthoom.rotateTimes < 0) { - kthoom.rotateTimes = 3; + if (hasModifier) break; + settings.rotateTimes--; + if (settings.rotateTimes < 0) { + settings.rotateTimes = 3; } updatePage(); break; case kthoom.Key.R: - kthoom.rotateTimes++; - if (kthoom.rotateTimes > 3) { - kthoom.rotateTimes = 0; + if (hasModifier) break; + settings.rotateTimes++; + if (settings.rotateTimes > 3) { + settings.rotateTimes = 0; } updatePage(); break; case kthoom.Key.F: - if (!hflip && !vflip) { - hflip = true; - } else if (hflip === true) { - vflip = true; - hflip = false; - } else if (vflip === true) { - vflip = false; + if (hasModifier) break; + if (!settings.hflip && !settings.vflip) { + settings.hflip = true; + } else if (settings.hflip === true && settings.vflip === true) { + settings.vflip = false; + settings.hflip = false; + } else if (settings.hflip === true) { + settings.vflip = true; + settings.hflip = false; + } else if (settings.vflip === true) { + settings.hflip = true; } updatePage(); break; case kthoom.Key.W: - fitMode = kthoom.Key.W; - updateScale(); + if (hasModifier) break; + settings.fitMode = kthoom.Key.W; + updateScale(false); break; case kthoom.Key.H: - fitMode = kthoom.Key.H; - updateScale(); + if (hasModifier) break; + settings.fitMode = kthoom.Key.H; + updateScale(false); break; case kthoom.Key.B: - fitMode = kthoom.Key.B; - updateScale(); + if (hasModifier) break; + settings.fitMode = kthoom.Key.B; + updateScale(false); break; case kthoom.Key.N: - fitMode = kthoom.Key.N; - updateScale(); + if (hasModifier) break; + settings.fitMode = kthoom.Key.N; + updateScale(false); + break; + case kthoom.Key.SPACE: + var container = $('#mainContent'); + var atTop = container.scrollTop() === 0; + var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height(); + + if (evt.shiftKey && atTop) { + evt.preventDefault(); + // If it's Shift + Space and the container is at the top of the page + showPrevPage(); + } else if (!evt.shiftKey && atBottom) { + evt.preventDefault(); + // If you're at the bottom of the page and you only pressed space + showNextPage(); + container.scrollTop(0); + } break; default: - //console.log('KeyCode = ' + code); + //console.log('KeyCode', evt.keyCode); break; } } -function init(filename) { - if (!window.FileReader) { - alert("Sorry, kthoom will not work with your browser because it does not support the File API. Please try kthoom with Chrome 12+ or Firefox 7+"); +/*function ImageLoadCallback() { + var jso = this.response; + // Unable to decompress file, or no response from server + if (jso === null) { + setImage("error"); } else { - var request = new XMLHttpRequest(); - request.open("GET", filename); - request.responseType = "arraybuffer"; - request.setRequestHeader("X-Test", "test1"); - request.setRequestHeader("X-Test", "test2"); - request.addEventListener("load", function(event) { - if (request.status >= 200 && request.status < 300) { - loadFromArrayBuffer(request.response); - } else { - console.warn(request.statusText, request.responseText); - } - }); - request.send(); - kthoom.initProgressMeter(); - document.body.className += /AppleWebKit/.test(navigator.userAgent) ? " webkit" : ""; - updateScale(true); - kthoom.loadSettings(); - $(document).keydown(keyHandler); + // IE 11 sometimes sees the response as a string + if (typeof jso !== "object") { + jso = JSON.parse(jso); + } - $(window).resize(function() { - var f = (screen.width - innerWidth < 4 && screen.height - innerHeight < 4); - getElem("titlebar").className = f ? "main" : ""; - updateScale(); + if (jso.page !== jso.last) { + this.open("GET", this.fileid + "/" + (jso.page + 1)); + this.addEventListener("load", ImageLoadCallback); + this.send(); + } + + loadFromArrayBuffer(jso); + } +}*/ +function init(filename) { + var request = new XMLHttpRequest(); + request.open("GET", filename); + request.responseType = "arraybuffer"; + request.setRequestHeader("X-Test", "test1"); + request.setRequestHeader("X-Test", "test2"); + request.addEventListener("load", function(event) { + if (request.status >= 200 && request.status < 300) { + loadFromArrayBuffer(request.response); + } else { + console.warn(request.statusText, request.responseText); + } + }); + request.send(); + initProgressClick(); + document.body.className += /AppleWebKit/.test(navigator.userAgent) ? " webkit" : ""; + kthoom.loadSettings(); + updateScale(true); + + $(document).keydown(keyHandler); + + $(window).resize(function() { + updateScale(false); + }); + + // Open TOC menu + $("#slider").click(function() { + $("#sidebar").toggleClass("open"); + $("#main").toggleClass("closed"); + $(this).toggleClass("icon-menu icon-right"); + + // We need this in a timeout because if we call it during the CSS transition, IE11 shakes the page ¯\_(ツ)_/¯ + setTimeout(function(){ + // Focus on the TOC or the main content area, depending on which is open + $('#main:not(.closed) #mainContent, #sidebar.open #tocView').focus(); + scrollTocToActive(); + }, 500); + }); + + // Open Settings modal + $("#setting").click(function() { + $("#settings-modal").toggleClass("md-show"); + }); + + // On Settings input change + $("#settings input").on("change", function() { + // Get either the checked boolean or the assigned value + var value = this.type === "checkbox" ? this.checked : this.value; + + // If it's purely numeric, parse it to an integer + value = /^\d+$/.test(value) ? parseInt(value) : value; + + settings[this.name] = value; + updatePage(); + updateScale(false); + }); + + // Close modal + $(".closer, .overlay").click(function() { + $(".md-show").removeClass("md-show"); + }); + + // TOC thumbnail pagination + $("#thumbnails").on("click", "a", function() { + currentImage = $(this).data("page") - 1; + updatePage(); + }); + + // Fullscreen mode + if (typeof screenfull !== "undefined") { + $("#fullscreen").click(function() { + screenfull.toggle($("#container")[0]); }); - $("#mainImage").click(function(evt) { - // Firefox does not support offsetX/Y so we have to manually calculate - // where the user clicked in the image. - var mainContentWidth = $("#mainContent").width(); - var mainContentHeight = $("#mainContent").height(); - var comicWidth = evt.target.clientWidth; - var comicHeight = evt.target.clientHeight; - var offsetX = (mainContentWidth - comicWidth) / 2; - var offsetY = (mainContentHeight - comicHeight) / 2; - var clickX = !!evt.offsetX ? evt.offsetX : (evt.clientX - offsetX); - var clickY = !!evt.offsetY ? evt.offsetY : (evt.clientY - offsetY); - - // Determine if the user clicked/tapped the left side or the - // right side of the page. - var clickedPrev = false; - switch (kthoom.rotateTimes) { + if (screenfull.raw) { + var $button = $("#fullscreen"); + document.addEventListener(screenfull.raw.fullscreenchange, function() { + screenfull.isFullscreen + ? $button.addClass("icon-resize-small").removeClass("icon-resize-full") + : $button.addClass("icon-resize-full").removeClass("icon-resize-small"); + }); + } + } + + // Focus the scrollable area so that keyboard scrolling work as expected + $('#mainContent').focus(); + + $("#mainImage").click(function(evt) { + // Firefox does not support offsetX/Y so we have to manually calculate + // where the user clicked in the image. + var mainContentWidth = $("#mainContent").width(); + var mainContentHeight = $("#mainContent").height(); + var comicWidth = evt.target.clientWidth; + var comicHeight = evt.target.clientHeight; + var offsetX = (mainContentWidth - comicWidth) / 2; + var offsetY = (mainContentHeight - comicHeight) / 2; + var clickX = evt.offsetX ? evt.offsetX : (evt.clientX - offsetX); + var clickY = evt.offsetY ? evt.offsetY : (evt.clientY - offsetY); + + // Determine if the user clicked/tapped the left side or the + // right side of the page. + var clickedPrev = false; + switch (settings.rotateTimes) { case 0: - clickedPrev = clickX < (comicWidth / 2); - break; + clickedPrev = clickX < (comicWidth / 2); + break; case 1: - clickedPrev = clickY < (comicHeight / 2); - break; + clickedPrev = clickY < (comicHeight / 2); + break; case 2: - clickedPrev = clickX > (comicWidth / 2); - break; + clickedPrev = clickX > (comicWidth / 2); + break; case 3: - clickedPrev = clickY > (comicHeight / 2); - break; - } - if (clickedPrev) { - showPrevPage(); - } else { - showNextPage(); - } - }); - } + clickedPrev = clickY > (comicHeight / 2); + break; + } + if (clickedPrev) { + showPrevPage(); + } else { + showNextPage(); + } + }); } + diff --git a/cps/static/js/libs/bootstrap-table/bootstrap-editable.min.js b/cps/static/js/libs/bootstrap-table/bootstrap-editable.min.js new file mode 100644 index 00000000..0511799c --- /dev/null +++ b/cps/static/js/libs/bootstrap-table/bootstrap-editable.min.js @@ -0,0 +1,7 @@ +/*! X-editable - v1.5.3 +* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery +* http://github.com/vitalets/x-editable +* Copyright (c) 2015 Vitaliy Potapov; Licensed MIT */ +!function(a){"use strict";var b=function(b,c){this.options=a.extend({},a.fn.editableform.defaults,c),this.$div=a(b),this.options.scope||(this.options.scope=this)};b.prototype={constructor:b,initInput:function(){this.input=this.options.input,this.value=this.input.str2value(this.options.value),this.input.prerender()},initTemplate:function(){this.$form=a(a.fn.editableform.template)},initButtons:function(){var b=this.$form.find(".editable-buttons");b.append(a.fn.editableform.buttons),"bottom"===this.options.showbuttons&&b.addClass("editable-buttons-bottom")},render:function(){this.$loading=a(a.fn.editableform.loading),this.$div.empty().append(this.$loading),this.initTemplate(),this.options.showbuttons?this.initButtons():this.$form.find(".editable-buttons").remove(),this.showLoading(),this.isSaving=!1,this.$div.triggerHandler("rendering"),this.initInput(),this.$form.find("div.editable-input").append(this.input.$tpl),this.$div.append(this.$form),a.when(this.input.render()).then(a.proxy(function(){if(this.options.showbuttons||this.input.autosubmit(),this.$form.find(".editable-cancel").click(a.proxy(this.cancel,this)),this.input.error)this.error(this.input.error),this.$form.find(".editable-submit").attr("disabled",!0),this.input.$input.attr("disabled",!0),this.$form.submit(function(a){a.preventDefault()});else{this.error(!1),this.input.$input.removeAttr("disabled"),this.$form.find(".editable-submit").removeAttr("disabled");var b=null===this.value||void 0===this.value||""===this.value?this.options.defaultValue:this.value;this.input.value2input(b),this.$form.submit(a.proxy(this.submit,this))}this.$div.triggerHandler("rendered"),this.showForm(),this.input.postrender&&this.input.postrender()},this))},cancel:function(){this.$div.triggerHandler("cancel")},showLoading:function(){var a,b;this.$form?(a=this.$form.outerWidth(),b=this.$form.outerHeight(),a&&this.$loading.width(a),b&&this.$loading.height(b),this.$form.hide()):(a=this.$loading.parent().width(),a&&this.$loading.width(a)),this.$loading.show()},showForm:function(a){this.$loading.hide(),this.$form.show(),a!==!1&&this.input.activate(),this.$div.triggerHandler("show")},error:function(b){var c,d=this.$form.find(".control-group"),e=this.$form.find(".editable-error-block");if(b===!1)d.removeClass(a.fn.editableform.errorGroupClass),e.removeClass(a.fn.editableform.errorBlockClass).empty().hide();else{if(b){c=(""+b).split("\n");for(var f=0;f").text(c[f]).html();b=c.join("
    ")}d.addClass(a.fn.editableform.errorGroupClass),e.addClass(a.fn.editableform.errorBlockClass).html(b).show()}},submit:function(b){b.stopPropagation(),b.preventDefault();var c=this.input.input2value(),d=this.validate(c);if("object"===a.type(d)&&void 0!==d.newValue){if(c=d.newValue,this.input.value2input(c),"string"==typeof d.msg)return this.error(d.msg),void this.showForm()}else if(d)return this.error(d),void this.showForm();if(!this.options.savenochange&&this.input.value2str(c)===this.input.value2str(this.value))return void this.$div.triggerHandler("nochange");var e=this.input.value2submit(c);this.isSaving=!0,a.when(this.save(e)).done(a.proxy(function(a){this.isSaving=!1;var b="function"==typeof this.options.success?this.options.success.call(this.options.scope,a,c):null;return b===!1?(this.error(!1),void this.showForm(!1)):"string"==typeof b?(this.error(b),void this.showForm()):(b&&"object"==typeof b&&b.hasOwnProperty("newValue")&&(c=b.newValue),this.error(!1),this.value=c,void this.$div.triggerHandler("save",{newValue:c,submitValue:e,response:a}))},this)).fail(a.proxy(function(a){this.isSaving=!1;var b;b="function"==typeof this.options.error?this.options.error.call(this.options.scope,a,c):"string"==typeof a?a:a.responseText||a.statusText||"Unknown error!",this.error(b),this.showForm()},this))},save:function(b){this.options.pk=a.fn.editableutils.tryParseJson(this.options.pk,!0);var c,d="function"==typeof this.options.pk?this.options.pk.call(this.options.scope):this.options.pk,e=!!("function"==typeof this.options.url||this.options.url&&("always"===this.options.send||"auto"===this.options.send&&null!==d&&void 0!==d));return e?(this.showLoading(),c={name:this.options.name||"",value:b,pk:d},"function"==typeof this.options.params?c=this.options.params.call(this.options.scope,c):(this.options.params=a.fn.editableutils.tryParseJson(this.options.params,!0),a.extend(c,this.options.params)),"function"==typeof this.options.url?this.options.url.call(this.options.scope,c):a.ajax(a.extend({url:this.options.url,data:c,type:"POST"},this.options.ajaxOptions))):void 0},validate:function(a){return void 0===a&&(a=this.value),"function"==typeof this.options.validate?this.options.validate.call(this.options.scope,a):void 0},option:function(a,b){a in this.options&&(this.options[a]=b),"value"===a&&this.setValue(b)},setValue:function(a,b){b?this.value=this.input.str2value(a):this.value=a,this.$form&&this.$form.is(":visible")&&this.input.value2input(this.value)}},a.fn.editableform=function(c){var d=arguments;return this.each(function(){var e=a(this),f=e.data("editableform"),g="object"==typeof c&&c;f||e.data("editableform",f=new b(this,g)),"string"==typeof c&&f[c].apply(f,Array.prototype.slice.call(d,1))})},a.fn.editableform.Constructor=b,a.fn.editableform.defaults={type:"text",url:null,params:null,name:null,pk:null,value:null,defaultValue:null,send:"auto",validate:null,success:null,error:null,ajaxOptions:null,showbuttons:!0,scope:null,savenochange:!1},a.fn.editableform.template='
    ',a.fn.editableform.loading='
    ',a.fn.editableform.buttons='',a.fn.editableform.errorGroupClass=null,a.fn.editableform.errorBlockClass="editable-error",a.fn.editableform.engine="jquery"}(window.jQuery),function(a){"use strict";a.fn.editableutils={inherit:function(a,b){var c=function(){};c.prototype=b.prototype,a.prototype=new c,a.prototype.constructor=a,a.superclass=b.prototype},setCursorPosition:function(a,b){if(a.setSelectionRange)a.setSelectionRange(b,b);else if(a.createTextRange){var c=a.createTextRange();c.collapse(!0),c.moveEnd("character",b),c.moveStart("character",b),c.select()}},tryParseJson:function(a,b){if("string"==typeof a&&a.length&&a.match(/^[\{\[].*[\}\]]$/))if(b)try{a=new Function("return "+a)()}catch(c){}finally{return a}else a=new Function("return "+a)();return a},sliceObj:function(b,c,d){var e,f,g={};if(!a.isArray(c)||!c.length)return g;for(var h=0;h").text(b).html()},itemsByValue:function(b,c,d){if(!c||null===b)return[];if("function"!=typeof d){var e=d||"value";d=function(a){return a[e]}}var f=a.isArray(b),g=[],h=this;return a.each(c,function(c,e){if(e.children)g=g.concat(h.itemsByValue(b,e.children,d));else if(f)a.grep(b,function(a){return a==(e&&"object"==typeof e?d(e):e)}).length&&g.push(e);else{var i=e&&"object"==typeof e?d(e):e;b==i&&g.push(e)}}),g},createInput:function(b){var c,d,e,f=b.type;return"date"===f&&("inline"===b.mode?a.fn.editabletypes.datefield?f="datefield":a.fn.editabletypes.dateuifield&&(f="dateuifield"):a.fn.editabletypes.date?f="date":a.fn.editabletypes.dateui&&(f="dateui"),"date"!==f||a.fn.editabletypes.date||(f="combodate")),"datetime"===f&&"inline"===b.mode&&(f="datetimefield"),"wysihtml5"!==f||a.fn.editabletypes[f]||(f="textarea"),"function"==typeof a.fn.editabletypes[f]?(c=a.fn.editabletypes[f],d=this.sliceObj(b,this.objectKeys(c.defaults)),e=new c(d)):(a.error("Unknown type: "+f),!1)},supportsTransitions:function(){var a=document.body||document.documentElement,b=a.style,c="transition",d=["Moz","Webkit","Khtml","O","ms"];if("string"==typeof b[c])return!0;c=c.charAt(0).toUpperCase()+c.substr(1);for(var e=0;e"),this.tip().is(this.innerCss)?this.tip().append(this.$form):this.tip().find(this.innerCss).append(this.$form),this.renderForm()},hide:function(a){if(this.tip()&&this.tip().is(":visible")&&this.$element.hasClass("editable-open")){if(this.$form.data("editableform").isSaving)return void(this.delayedHide={reason:a});this.delayedHide=!1,this.$element.removeClass("editable-open"),this.innerHide(),this.$element.triggerHandler("hidden",a||"manual")}},innerShow:function(){},innerHide:function(){},toggle:function(a){this.container()&&this.tip()&&this.tip().is(":visible")?this.hide():this.show(a)},setPosition:function(){},save:function(a,b){this.$element.triggerHandler("save",b),this.hide("save")},option:function(a,b){this.options[a]=b,a in this.containerOptions?(this.containerOptions[a]=b,this.setContainerOption(a,b)):(this.formOptions[a]=b,this.$form&&this.$form.editableform("option",a,b))},setContainerOption:function(a,b){this.call("option",a,b)},destroy:function(){this.hide(),this.innerDestroy(),this.$element.off("destroyed"),this.$element.removeData("editableContainer")},innerDestroy:function(){},closeOthers:function(b){a(".editable-open").each(function(c,d){if(d!==b&&!a(d).find(b).length){var e=a(d),f=e.data("editableContainer");f&&("cancel"===f.options.onblur?e.data("editableContainer").hide("onblur"):"submit"===f.options.onblur&&e.data("editableContainer").tip().find("form").submit())}})},activate:function(){this.tip&&this.tip().is(":visible")&&this.$form&&this.$form.data("editableform").input.activate()}},a.fn.editableContainer=function(d){var e=arguments;return this.each(function(){var f=a(this),g="editableContainer",h=f.data(g),i="object"==typeof d&&d,j="inline"===i.mode?c:b;h||f.data(g,h=new j(this,i)),"string"==typeof d&&h[d].apply(h,Array.prototype.slice.call(e,1))})},a.fn.editableContainer.Popup=b,a.fn.editableContainer.Inline=c,a.fn.editableContainer.defaults={value:null,placement:"top",autohide:!0,onblur:"cancel",anim:!1,mode:"popup"},jQuery.event.special.destroyed={remove:function(a){a.handler&&a.handler()}}}(window.jQuery),function(a){"use strict";a.extend(a.fn.editableContainer.Inline.prototype,a.fn.editableContainer.Popup.prototype,{containerName:"editableform",innerCss:".editable-inline",containerClass:"editable-container editable-inline",initContainer:function(){this.$tip=a(""),this.options.anim||(this.options.anim=0)},splitOptions:function(){this.containerOptions={},this.formOptions=this.options},tip:function(){return this.$tip},innerShow:function(){this.$element.hide(),this.tip().insertAfter(this.$element).show()},innerHide:function(){this.$tip.hide(this.options.anim,a.proxy(function(){this.$element.show(),this.innerDestroy()},this))},innerDestroy:function(){this.tip()&&this.tip().empty().remove()}})}(window.jQuery),function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.editable.defaults,c,a.fn.editableutils.getConfigData(this.$element)),this.options.selector?this.initLive():this.init(),this.options.highlight&&!a.fn.editableutils.supportsTransitions()&&(this.options.highlight=!1)};b.prototype={constructor:b,init:function(){var b,c=!1;if(this.options.name=this.options.name||this.$element.attr("id"),this.options.scope=this.$element[0],this.input=a.fn.editableutils.createInput(this.options),this.input){switch(void 0===this.options.value||null===this.options.value?(this.value=this.input.html2value(a.trim(this.$element.html())),c=!0):(this.options.value=a.fn.editableutils.tryParseJson(this.options.value,!0),"string"==typeof this.options.value?this.value=this.input.str2value(this.options.value):this.value=this.options.value),this.$element.addClass("editable"),"textarea"===this.input.type&&this.$element.addClass("editable-pre-wrapped"),"manual"!==this.options.toggle?(this.$element.addClass("editable-click"),this.$element.on(this.options.toggle+".editable",a.proxy(function(a){if(this.options.disabled||a.preventDefault(),"mouseenter"===this.options.toggle)this.show();else{var b="click"!==this.options.toggle;this.toggle(b)}},this))):this.$element.attr("tabindex",-1),"function"==typeof this.options.display&&(this.options.autotext="always"),this.options.autotext){case"always":b=!0;break;case"auto":b=!a.trim(this.$element.text()).length&&null!==this.value&&void 0!==this.value&&!c;break;default:b=!1}a.when(b?this.render():!0).then(a.proxy(function(){this.options.disabled?this.disable():this.enable(),this.$element.triggerHandler("init",this)},this))}},initLive:function(){var b=this.options.selector;this.options.selector=!1,this.options.autotext="never",this.$element.on(this.options.toggle+".editable",b,a.proxy(function(b){var c=a(b.target);c.data("editable")||(c.hasClass(this.options.emptyclass)&&c.empty(),c.editable(this.options).trigger(b))},this))},render:function(a){return this.options.display!==!1?this.input.value2htmlFinal?this.input.value2html(this.value,this.$element[0],this.options.display,a):"function"==typeof this.options.display?this.options.display.call(this.$element[0],this.value,a):this.input.value2html(this.value,this.$element[0]):void 0},enable:function(){this.options.disabled=!1,this.$element.removeClass("editable-disabled"),this.handleEmpty(this.isEmpty),"manual"!==this.options.toggle&&"-1"===this.$element.attr("tabindex")&&this.$element.removeAttr("tabindex")},disable:function(){this.options.disabled=!0,this.hide(),this.$element.addClass("editable-disabled"),this.handleEmpty(this.isEmpty),this.$element.attr("tabindex",-1)},toggleDisabled:function(){this.options.disabled?this.enable():this.disable()},option:function(b,c){return b&&"object"==typeof b?void a.each(b,a.proxy(function(b,c){this.option(a.trim(b),c)},this)):(this.options[b]=c,"disabled"===b?c?this.disable():this.enable():("value"===b&&this.setValue(c),this.container&&this.container.option(b,c),void(this.input.option&&this.input.option(b,c))))},handleEmpty:function(b){this.options.display!==!1&&(void 0!==b?this.isEmpty=b:"function"==typeof this.input.isEmpty?this.isEmpty=this.input.isEmpty(this.$element):this.isEmpty=""===a.trim(this.$element.html()),this.options.disabled?this.isEmpty&&(this.$element.empty(),this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass)):this.isEmpty?(this.$element.html(this.options.emptytext),this.options.emptyclass&&this.$element.addClass(this.options.emptyclass)):this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass))},show:function(b){if(!this.options.disabled){if(this.container){if(this.container.tip().is(":visible"))return}else{var c=a.extend({},this.options,{value:this.value,input:this.input});this.$element.editableContainer(c),this.$element.on("save.internal",a.proxy(this.save,this)),this.container=this.$element.data("editableContainer")}this.container.show(b)}},hide:function(){this.container&&this.container.hide()},toggle:function(a){this.container&&this.container.tip().is(":visible")?this.hide():this.show(a)},save:function(a,b){if(this.options.unsavedclass){var c=!1;c=c||"function"==typeof this.options.url,c=c||this.options.display===!1,c=c||void 0!==b.response,c=c||this.options.savenochange&&this.input.value2str(this.value)!==this.input.value2str(b.newValue),c?this.$element.removeClass(this.options.unsavedclass):this.$element.addClass(this.options.unsavedclass)}if(this.options.highlight){var d=this.$element,e=d.css("background-color");d.css("background-color",this.options.highlight),setTimeout(function(){"transparent"===e&&(e=""),d.css("background-color",e),d.addClass("editable-bg-transition"),setTimeout(function(){d.removeClass("editable-bg-transition")},1700)},10)}this.setValue(b.newValue,!1,b.response)},validate:function(){return"function"==typeof this.options.validate?this.options.validate.call(this,this.value):void 0},setValue:function(b,c,d){c?this.value=this.input.str2value(b):this.value=b,this.container&&this.container.option("value",this.value),a.when(this.render(d)).then(a.proxy(function(){this.handleEmpty()},this))},activate:function(){this.container&&this.container.activate()},destroy:function(){this.disable(),this.container&&this.container.destroy(),this.input.destroy(),"manual"!==this.options.toggle&&(this.$element.removeClass("editable-click"),this.$element.off(this.options.toggle+".editable")),this.$element.off("save.internal"),this.$element.removeClass("editable editable-open editable-disabled"),this.$element.removeData("editable")}},a.fn.editable=function(c){var d={},e=arguments,f="editable";switch(c){case"validate":return this.each(function(){var b,c=a(this),e=c.data(f);e&&(b=e.validate())&&(d[e.options.name]=b)}),d;case"getValue":return 2===arguments.length&&arguments[1]===!0?d=this.eq(0).data(f).value:this.each(function(){var b=a(this),c=b.data(f);c&&void 0!==c.value&&null!==c.value&&(d[c.options.name]=c.input.value2submit(c.value))}),d;case"submit":var g=arguments[1]||{},h=this,i=this.editable("validate");if(a.isEmptyObject(i)){var j={};if(1===h.length){var k=h.data("editable"),l={name:k.options.name||"",value:k.input.value2submit(k.value),pk:"function"==typeof k.options.pk?k.options.pk.call(k.options.scope):k.options.pk};"function"==typeof k.options.params?l=k.options.params.call(k.options.scope,l):(k.options.params=a.fn.editableutils.tryParseJson(k.options.params,!0),a.extend(l,k.options.params)),j={url:k.options.url,data:l,type:"POST"},g.success=g.success||k.options.success,g.error=g.error||k.options.error}else{var m=this.editable("getValue");j={url:g.url,data:m,type:"POST"}}j.success="function"==typeof g.success?function(a){g.success.call(h,a,g)}:a.noop,j.error="function"==typeof g.error?function(){g.error.apply(h,arguments)}:a.noop,g.ajaxOptions&&a.extend(j,g.ajaxOptions),g.data&&a.extend(j.data,g.data),a.ajax(j)}else"function"==typeof g.error&&g.error.call(h,i);return this}return this.each(function(){var d=a(this),g=d.data(f),h="object"==typeof c&&c;return h&&h.selector?void(g=new b(this,h)):(g||d.data(f,g=new b(this,h)),void("string"==typeof c&&g[c].apply(g,Array.prototype.slice.call(e,1))))})},a.fn.editable.defaults={type:"text",disabled:!1,toggle:"click",emptytext:"Empty",autotext:"auto",value:null,display:null,emptyclass:"editable-empty",unsavedclass:"editable-unsaved",selector:null,highlight:"#FFFF80"}}(window.jQuery),function(a){"use strict";a.fn.editabletypes={};var b=function(){};b.prototype={init:function(b,c,d){this.type=b,this.options=a.extend({},d,c)},prerender:function(){this.$tpl=a(this.options.tpl),this.$input=this.$tpl,this.$clear=null,this.error=null},render:function(){},value2html:function(b,c){a(c)[this.options.escape?"text":"html"](a.trim(b))},html2value:function(b){return a("
    ").html(b).text()},value2str:function(a){return a},str2value:function(a){return a},value2submit:function(a){return a},value2input:function(a){this.$input.val(a)},input2value:function(){return this.$input.val()},activate:function(){this.$input.is(":visible")&&this.$input.focus()},clear:function(){this.$input.val(null)},escape:function(b){return a("
    ").text(b).html()},autosubmit:function(){},destroy:function(){},setClass:function(){this.options.inputclass&&this.$input.addClass(this.options.inputclass)},setAttr:function(a){void 0!==this.options[a]&&null!==this.options[a]&&this.$input.attr(a,this.options[a])},option:function(a,b){this.options[a]=b}},b.defaults={tpl:"",inputclass:null,escape:!0,scope:null,showbuttons:!0},a.extend(a.fn.editabletypes,{abstractinput:b})}(window.jQuery),function(a){"use strict";var b=function(a){};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){var b=a.Deferred();return this.error=null,this.onSourceReady(function(){this.renderList(),b.resolve()},function(){this.error=this.options.sourceError,b.resolve()}),b.promise()},html2value:function(a){return null},value2html:function(b,c,d,e){var f=a.Deferred(),g=function(){"function"==typeof d?d.call(c,b,this.sourceData,e):this.value2htmlFinal(b,c),f.resolve()};return null===b?g.call(this):this.onSourceReady(g,function(){f.resolve()}),f.promise()},onSourceReady:function(b,c){var d;if(a.isFunction(this.options.source)?(d=this.options.source.call(this.options.scope),this.sourceData=null):d=this.options.source,this.options.sourceCache&&a.isArray(this.sourceData))return void b.call(this);try{d=a.fn.editableutils.tryParseJson(d,!1)}catch(e){return void c.call(this)}if("string"==typeof d){if(this.options.sourceCache){var f,g=d;if(a(document).data(g)||a(document).data(g,{}),f=a(document).data(g),f.loading===!1&&f.sourceData)return this.sourceData=f.sourceData,this.doPrepend(),void b.call(this);if(f.loading===!0)return f.callbacks.push(a.proxy(function(){this.sourceData=f.sourceData,this.doPrepend(),b.call(this)},this)),void f.err_callbacks.push(a.proxy(c,this));f.loading=!0,f.callbacks=[],f.err_callbacks=[]}var h=a.extend({url:d,type:"get",cache:!1,dataType:"json",success:a.proxy(function(d){f&&(f.loading=!1),this.sourceData=this.makeArray(d),a.isArray(this.sourceData)?(f&&(f.sourceData=this.sourceData,a.each(f.callbacks,function(){this.call()})),this.doPrepend(),b.call(this)):(c.call(this),f&&a.each(f.err_callbacks,function(){this.call()}))},this),error:a.proxy(function(){c.call(this),f&&(f.loading=!1,a.each(f.err_callbacks,function(){this.call()}))},this)},this.options.sourceOptions);a.ajax(h)}else this.sourceData=this.makeArray(d),a.isArray(this.sourceData)?(this.doPrepend(),b.call(this)):c.call(this)},doPrepend:function(){null!==this.options.prepend&&void 0!==this.options.prepend&&(a.isArray(this.prependData)||(a.isFunction(this.options.prepend)&&(this.options.prepend=this.options.prepend.call(this.options.scope)),this.options.prepend=a.fn.editableutils.tryParseJson(this.options.prepend,!0),"string"==typeof this.options.prepend&&(this.options.prepend={"":this.options.prepend}),this.prependData=this.makeArray(this.options.prepend)),a.isArray(this.prependData)&&a.isArray(this.sourceData)&&(this.sourceData=this.prependData.concat(this.sourceData)))},renderList:function(){},value2htmlFinal:function(a,b){},makeArray:function(b){var c,d,e,f,g=[];if(!b||"string"==typeof b)return null;if(a.isArray(b)){f=function(a,b){return d={value:a,text:b},c++>=2?!1:void 0};for(var h=0;h1&&(e.children&&(e.children=this.makeArray(e.children)),g.push(e))):g.push({value:e,text:e})}else a.each(b,function(a,b){g.push({value:a,text:b})});return g},option:function(a,b){this.options[a]=b,"source"===a&&(this.sourceData=null),"prepend"===a&&(this.prependData=null)}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{source:null,prepend:!1,sourceError:"Error when loading list",sourceCache:!0,sourceOptions:null}),a.fn.editabletypes.list=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("text",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){this.renderClear(),this.setClass(),this.setAttr("placeholder")},activate:function(){this.$input.is(":visible")&&(this.$input.focus(),this.$input.is("input,textarea")&&!this.$input.is('[type="checkbox"],[type="range"]')&&a.fn.editableutils.setCursorPosition(this.$input.get(0),this.$input.val().length),this.toggleClear&&this.toggleClear())},renderClear:function(){this.options.clear&&(this.$clear=a(''),this.$input.after(this.$clear).css("padding-right",24).keyup(a.proxy(function(b){if(!~a.inArray(b.keyCode,[40,38,9,13,27])){clearTimeout(this.t);var c=this;this.t=setTimeout(function(){c.toggleClear(b)},100)}},this)).parent().css("position","relative"),this.$clear.click(a.proxy(this.clear,this)))},postrender:function(){},toggleClear:function(a){if(this.$clear){var b=this.$input.val().length,c=this.$clear.is(":visible");b&&!c&&this.$clear.show(),!b&&c&&this.$clear.hide()}},clear:function(){this.$clear.hide(),this.$input.val("").focus()}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:'',placeholder:null,clear:!0}),a.fn.editabletypes.text=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("textarea",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){this.setClass(),this.setAttr("placeholder"),this.setAttr("rows"),this.$input.keydown(function(b){b.ctrlKey&&13===b.which&&a(this).closest("form").submit()})},activate:function(){a.fn.editabletypes.text.prototype.activate.call(this)}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:"",inputclass:"input-large",placeholder:null,rows:7}),a.fn.editabletypes.textarea=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("select",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.list),a.extend(b.prototype,{renderList:function(){this.$input.empty();var b=this.options.escape,c=function(d,e){var f;if(a.isArray(e))for(var g=0;g",f),e[g].children));else{f.value=e[g].value,e[g].disabled&&(f.disabled=!0);var h=a("