├── .gitignore ├── img ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg ├── Qiu.png ├── edit.png ├── cursor.png ├── delete.png ├── edit-d.png ├── collapse.png ├── delete-d.png ├── cursor-move.png ├── left-arrow.png ├── right-arrow.png ├── vertical-page.png ├── horizontal-page.png ├── double-page.svg └── windmills.svg ├── css ├── epub │ ├── single-page.css │ └── common.css ├── font │ ├── font.woff2 │ ├── QldKNThLqRwH-OJ1UHjlKGlZ5qg.woff2 │ ├── QldKNThLqRwH-OJ1UHjlKGlW5qhWxg.woff2 │ ├── QldKNThLqRwH-OJ1UHjlKGlX5qhWxg.woff2 │ ├── 2fcrYFNaTjcS6g4U3t-Y5ZjZjT5FdEJ140U2DJYC3mY.woff2 │ └── icon-font.css ├── modules │ ├── background.css │ ├── tab.css │ ├── form.css │ ├── common.css │ ├── icon.css │ ├── color.css │ ├── modal.css │ └── button.css ├── base │ └── reset.css └── pages │ ├── index.css │ └── reader.css ├── manifest.json ├── README.md ├── js ├── background.js ├── lib │ ├── rangy-1.3.0 │ │ ├── LICENSE.txt │ │ ├── rangy-selectionsaverestore.js │ │ ├── rangy-serializer.js │ │ └── rangy-highlighter.js │ ├── epub.js │ │ └── LICENSE.txt │ └── JSZip │ │ └── LICENSE.markdown ├── currentLocation.js ├── epub │ ├── locate.js │ └── selection.js ├── highlight.js ├── bookDB.js ├── bookMark.js ├── modal.js ├── drag.js ├── setting.js ├── colorpicker.js ├── tab.js └── index.js ├── index.html ├── user-stylesheet └── light.css └── reader.html /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | \.idea/ 3 | -------------------------------------------------------------------------------- /img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/1.jpg -------------------------------------------------------------------------------- /img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/2.jpg -------------------------------------------------------------------------------- /img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/3.jpg -------------------------------------------------------------------------------- /img/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/4.jpg -------------------------------------------------------------------------------- /img/Qiu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/Qiu.png -------------------------------------------------------------------------------- /img/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/edit.png -------------------------------------------------------------------------------- /css/epub/single-page.css: -------------------------------------------------------------------------------- 1 | html { 2 | width: 700px; 3 | margin: auto; 4 | } -------------------------------------------------------------------------------- /img/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/cursor.png -------------------------------------------------------------------------------- /img/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/delete.png -------------------------------------------------------------------------------- /img/edit-d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/edit-d.png -------------------------------------------------------------------------------- /img/collapse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/collapse.png -------------------------------------------------------------------------------- /img/delete-d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/delete-d.png -------------------------------------------------------------------------------- /css/font/font.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/css/font/font.woff2 -------------------------------------------------------------------------------- /img/cursor-move.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/cursor-move.png -------------------------------------------------------------------------------- /img/left-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/left-arrow.png -------------------------------------------------------------------------------- /img/right-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/right-arrow.png -------------------------------------------------------------------------------- /img/vertical-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/vertical-page.png -------------------------------------------------------------------------------- /img/horizontal-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/img/horizontal-page.png -------------------------------------------------------------------------------- /css/font/QldKNThLqRwH-OJ1UHjlKGlZ5qg.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/css/font/QldKNThLqRwH-OJ1UHjlKGlZ5qg.woff2 -------------------------------------------------------------------------------- /css/font/QldKNThLqRwH-OJ1UHjlKGlW5qhWxg.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/css/font/QldKNThLqRwH-OJ1UHjlKGlW5qhWxg.woff2 -------------------------------------------------------------------------------- /css/font/QldKNThLqRwH-OJ1UHjlKGlX5qhWxg.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/css/font/QldKNThLqRwH-OJ1UHjlKGlX5qhWxg.woff2 -------------------------------------------------------------------------------- /css/font/2fcrYFNaTjcS6g4U3t-Y5ZjZjT5FdEJ140U2DJYC3mY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qixiaoo/QiuReader/HEAD/css/font/2fcrYFNaTjcS6g4U3t-Y5ZjZjT5FdEJ140U2DJYC3mY.woff2 -------------------------------------------------------------------------------- /css/modules/background.css: -------------------------------------------------------------------------------- 1 | /* background */ 2 | 3 | .bg-1 { 4 | background-image: url(../../img/1.jpg); 5 | } 6 | 7 | .bg-2 { 8 | background-image: url(../../img/2.jpg); 9 | } 10 | 11 | .bg-3 { 12 | background-image: url(../../img/3.jpg); 13 | } 14 | 15 | .bg-4 { 16 | background-image: url(../../img/4.jpg); 17 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "QiuReader", 4 | "version": "0.1.4", 5 | "description": "a simple epub reader", 6 | "icons": { 7 | "128": "img/Qiu.png" 8 | }, 9 | "browser_action": { 10 | "default_icon": { 11 | "30": "img/Qiu.png" 12 | }, 13 | "default_title": "QiuReader" 14 | }, 15 | "background": { 16 | "scripts": ["js/background.js"] 17 | }, 18 | "permissions": [ 19 | "tabs" 20 | ] 21 | } -------------------------------------------------------------------------------- /css/modules/tab.css: -------------------------------------------------------------------------------- 1 | .tabs { 2 | position: relative; 3 | height: 48px; 4 | width: 100%; 5 | margin: auto; 6 | display: flex; 7 | } 8 | 9 | .tab { 10 | height: inherit; 11 | line-height: 48px; 12 | text-align: center; 13 | flex-grow: 1; 14 | cursor: pointer; 15 | } 16 | 17 | .tabs .tab a { 18 | height: 100%; 19 | display: block; 20 | font-size: 14px; 21 | } 22 | 23 | .tabs .tab-slider { 24 | position: absolute; 25 | bottom: 0; 26 | height: 2px; 27 | background-color: #f5c18a; 28 | transition: .3s; 29 | } 30 | 31 | .tab-panel { 32 | margin: auto; 33 | } -------------------------------------------------------------------------------- /css/modules/form.css: -------------------------------------------------------------------------------- 1 | input[type='text'] { 2 | font-size: 1rem; 3 | padding: .35em .5em; 4 | border: 1px solid #e0e0e0; 5 | border-radius: .25em; 6 | background-color: #fdfdfd; 7 | transition: .3s; 8 | } 9 | 10 | input[type='text']:focus { 11 | background-color: #fff; 12 | box-shadow: 0 0 8px 0 rgba(28, 186, 163, 0.35); 13 | } 14 | 15 | .input-group { 16 | width: 100%; 17 | margin-bottom: 10px; 18 | } 19 | 20 | .input-group > label { 21 | display: inline-block; 22 | width: 20%; 23 | margin-right: 1.5rem; 24 | text-align: right; 25 | } 26 | 27 | .input-group > input { 28 | width: 50%; 29 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Deprecated. 2 | 3 | # QiuReader 4 | 5 | QiuReader 是一个仅在 Chrome 和 Firefox 下可用的简单的 epub 阅读器,由 [epub.js](https://github.com/futurepress/epub.js) 、 [Rangy](https://github.com/timdown/rangy) 驱动。 6 | 7 | 8 | 9 | ## 介绍 10 | 11 | **Warning**:这是一个学习前端的练手项目,限于作者姿势水平,在使用过程中可能会出现不可避免的崩坏、失控、闪瞎眼等情况,欢迎用问题来鞭挞我 ᕕ( ᐛ )ᕗ 。 12 | 13 | 14 | 15 | ## 使用方式 16 | 17 | 1. 可以通过访问 [QiuReader](https://qixiaoo.github.io/QiuReader) 在线阅读。 18 | 2. 通过 github 的“Download ZIP”功能下载到本地,解压后放在自己的服务器上,通过在浏览器下访问 `你的服务器地址/解压文件夹/index.html` 即可使用。 19 | 3. 通过安装 [Firefox 插件](https://addons.mozilla.org/zh-CN/firefox/addon/qiureader/) 使用。 20 | 4. 后期**可能**会以 chrome 插件的形式发布。 21 | 22 | 23 | -------------------------------------------------------------------------------- /js/background.js: -------------------------------------------------------------------------------- 1 | var isFirefox = navigator.userAgent.indexOf('Firefox') !== -1; 2 | 3 | // for Firefox 4 | if (isFirefox) { 5 | browser.browserAction.onClicked.addListener( 6 | function () { 7 | var url = browser.runtime.getURL('index.html'); 8 | browser.tabs.create({ 9 | url: url 10 | }); 11 | } 12 | ); 13 | } else { 14 | // for Chrome 15 | chrome.browserAction.onClicked.addListener( 16 | function () { 17 | var url = chrome.runtime.getURL('index.html'); 18 | chrome.tabs.create({ 19 | url: url 20 | }); 21 | } 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /css/modules/common.css: -------------------------------------------------------------------------------- 1 | /* layout */ 2 | 3 | .container { 4 | width: 75%; 5 | margin: auto; 6 | } 7 | 8 | .left { 9 | float: left; 10 | } 11 | 12 | .right { 13 | float: right; 14 | } 15 | 16 | .clear:before, 17 | .clear:after 18 | { 19 | content: ''; 20 | display: table; 21 | clear: both; 22 | } 23 | 24 | /* other */ 25 | 26 | .disable-select { 27 | -webkit-touch-callout: none; 28 | -webkit-user-select: none; 29 | -khtml-user-select: none; 30 | -moz-user-select: none; 31 | -ms-user-select: none; 32 | user-select: none; 33 | } 34 | 35 | .hidden { 36 | display: none; 37 | } 38 | 39 | .divider { 40 | height: 1px; 41 | background-color: #e0e0e0; 42 | } -------------------------------------------------------------------------------- /img/double-page.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /css/modules/icon.css: -------------------------------------------------------------------------------- 1 | /* icon */ 2 | 3 | [class*='icon-'] { 4 | display: inline-block; 5 | background: center no-repeat; 6 | min-width: 16px; 7 | min-height: 16px; 8 | margin-right: 5px; 9 | } 10 | 11 | .icon-delete { 12 | background-image: url(../../img/delete.png); 13 | } 14 | 15 | .icon-delete:hover { 16 | background-image: url(../../img/delete-d.png); 17 | } 18 | 19 | .icon-edit { 20 | background-image: url(../../img/edit.png); 21 | } 22 | 23 | .icon-edit:hover { 24 | background-image: url(../../img/edit-d.png); 25 | } 26 | 27 | .icon-horizontal-page { 28 | background-image: url("../../img/horizontal-page.png"); 29 | } 30 | 31 | .icon-double-page { 32 | background: url("../../img/double-page.svg"); 33 | } 34 | 35 | .icon-vertical-page { 36 | background-image: url("../../img/vertical-page.png"); 37 | } -------------------------------------------------------------------------------- /css/modules/color.css: -------------------------------------------------------------------------------- 1 | /* color */ 2 | .text-blue { 3 | color: #1FB2EA; 4 | } 5 | 6 | .text-dark-blue { 7 | color: #0470BA; 8 | } 9 | 10 | .text-green { 11 | color: #B6EFCA; 12 | } 13 | 14 | .text-dark-green { 15 | color: #7EE2CF; 16 | } 17 | 18 | .text-brown { 19 | color: #B1A29C; 20 | } 21 | 22 | .text-dark-brown { 23 | color: #A5938C; 24 | } 25 | 26 | .text-purple { 27 | color: #9874B8; 28 | } 29 | 30 | .text-dark-purple { 31 | color: #796095; 32 | } 33 | 34 | .text-orange { 35 | color: #F5C28A; 36 | } 37 | 38 | .text-dark-orange { 39 | color: #F5AD8A; 40 | } 41 | 42 | .bg-red { 43 | background-color: #FFBA84; 44 | } 45 | 46 | .bg-orange { 47 | background-color: #E2943B; 48 | } 49 | 50 | .bg-yellow { 51 | background-color: #F7C242; 52 | } 53 | 54 | .bg-green { 55 | background-color: #86C166; 56 | } 57 | 58 | .bg-blue { 59 | background-color: #33A6B8; 60 | } 61 | 62 | .bg-purple { 63 | background-color: #8A6BBE; 64 | } -------------------------------------------------------------------------------- /css/epub/common.css: -------------------------------------------------------------------------------- 1 | ::selection { 2 | background: #00ad96; 3 | color: #fff; 4 | } 5 | 6 | ::-moz-selection { 7 | background: #00ad96; 8 | color: #fff; 9 | } 10 | 11 | /* annotation highlight */ 12 | 13 | [class*=hl-] { 14 | color: #FFF; 15 | } 16 | 17 | .hl-red { 18 | background-color: #FFBA84; 19 | } 20 | 21 | .hl-orange { 22 | background-color: #E2943B; 23 | } 24 | 25 | .hl-yellow { 26 | background-color: #F7C242; 27 | } 28 | 29 | .hl-green { 30 | background-color: #86C166; 31 | } 32 | 33 | .hl-blue { 34 | background-color: #33A6B8; 35 | } 36 | 37 | .hl-purple { 38 | background-color: #8A6BBE; 39 | } 40 | 41 | [class*=line-] { 42 | border-bottom: .125em solid; 43 | } 44 | 45 | .line-red { 46 | border-color: #FFBA84; 47 | } 48 | 49 | .line-orange { 50 | border-color: #E2943B; 51 | } 52 | 53 | .line-yellow { 54 | border-color: #F7C242; 55 | } 56 | 57 | .line-green { 58 | border-color: #86C166; 59 | } 60 | 61 | .line-blue { 62 | border-color: #33A6B8; 63 | } 64 | 65 | .line-purple { 66 | border-color: #8A6BBE; 67 | } -------------------------------------------------------------------------------- /js/lib/rangy-1.3.0/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tim Down 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /css/base/reset.css: -------------------------------------------------------------------------------- 1 | html { 2 | -ms-text-size-adjust: 100%; 3 | -webkit-text-size-adjust: 100%; 4 | } 5 | 6 | body { 7 | margin: 0; 8 | font: 16px/1.5 sans-serif; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | } 12 | 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | p, 18 | blockquote, 19 | figure, 20 | ol, 21 | ul { 22 | margin: 0; 23 | padding: 0; 24 | } 25 | 26 | main, 27 | li { 28 | display: block; 29 | } 30 | 31 | h1, 32 | h2, 33 | h3, 34 | h4, 35 | h5, 36 | h6 { 37 | font-size: inherit; 38 | } 39 | 40 | strong { 41 | font-weight: bold; 42 | } 43 | 44 | a, 45 | button { 46 | color: inherit; 47 | transition: .3s; 48 | } 49 | 50 | a { 51 | text-decoration: none; 52 | } 53 | 54 | button { 55 | overflow: visible; 56 | border: 0; 57 | font: inherit; 58 | -webkit-font-smoothing: inherit; 59 | letter-spacing: inherit; 60 | background: none; 61 | cursor: pointer; 62 | } 63 | 64 | ::-moz-focus-inner { 65 | padding: 0; 66 | border: 0; 67 | } 68 | 69 | :focus { 70 | outline: 0; 71 | } 72 | 73 | img { 74 | max-width: 100%; 75 | height: auto; 76 | border: 0; 77 | } -------------------------------------------------------------------------------- /js/currentLocation.js: -------------------------------------------------------------------------------- 1 | // 记录书籍的阅读进度信息 2 | var currentLocation = { 3 | 4 | // 分隔符 5 | separator: ' qiu-separator ', 6 | 7 | recordCurrentLocation: function () { 8 | var bookKey = localStorage.getItem('reading'); 9 | var currentCfi = book.getCurrentLocationCfi(); 10 | var currentY = getYScroll(); 11 | var current = currentCfi + this.separator + currentY; 12 | localStorage.setItem('currentLocationCfi' + bookKey, current); 13 | }, 14 | 15 | getCurrentLocation: function () { 16 | var bookKey = localStorage.getItem('reading'); 17 | 18 | for (var i = 0; i < localStorage.length; i++) { 19 | if (localStorage.key(i) === ('currentLocationCfi' + bookKey)) { 20 | var value = localStorage.getItem(localStorage.key(i)); 21 | var result = {}; 22 | result.cfi = value.split(this.separator)[0]; 23 | result.posY = value.split(this.separator)[1]; 24 | 25 | return result; 26 | } 27 | } 28 | 29 | return false; 30 | }, 31 | 32 | // 清除特定书籍的进度信息 33 | clear: function (bookKey) { 34 | var i; 35 | 36 | for (i = 0; i < localStorage.length; i++) { 37 | if (localStorage.key(i) === ('currentLocationCfi' + bookKey)) { 38 | localStorage.removeItem(localStorage.key(i)); 39 | break; 40 | } 41 | } 42 | } 43 | }; -------------------------------------------------------------------------------- /css/modules/modal.css: -------------------------------------------------------------------------------- 1 | /* 2 | * modal.css 可以在页面上创建一个模态框,需要与 modal.js 一起使用 3 | * 4 | * 使用说明: 5 | * 6 | * 首先需要在 HTML 中引入文件 modal.css 和 modal.js 7 | * 然后,使用时需要在 标签下创建一个
8 | * 9 | * 另外,你可以为一个元素加上 .modal-trigger 类,就可以把这个元素作为打开模态框的触发器 10 | * 同理,加上 .modal-close 类的元素可以在被点击后关闭模态框 11 | * 注意:你还要为上面的元素加上 modal-target 属性,属性值为要打开/关闭的模态框的id 12 | */ 13 | 14 | .modal { 15 | position: fixed; 16 | top: 20%; 17 | left: 0; 18 | right: 0; 19 | margin: auto; 20 | padding: 20px; 21 | max-width: 50vw; 22 | max-height: 70vh; 23 | border-radius: 5px; 24 | background-color: white; 25 | overflow-y: auto; 26 | box-shadow: 0 2px 2px 1px #666; 27 | opacity: 0; 28 | visibility: hidden; 29 | z-index: 999; 30 | transform: scale(0.7, 0.7); 31 | transition-property: all; 32 | transition-duration: .5s; 33 | } 34 | 35 | .modal-header { 36 | font-size: 21px; 37 | text-align: center; 38 | margin: 0 auto 15px; 39 | } 40 | 41 | .modal-content { 42 | margin: auto; 43 | } 44 | 45 | .modal-footer { 46 | text-align: center; 47 | margin: 15px auto 0; 48 | } 49 | 50 | .modal-overlay { 51 | position: fixed; 52 | left: 0; 53 | top: 0; 54 | width: 100vw; 55 | height: 100vh; 56 | background-color: #000; 57 | opacity: 0; 58 | visibility: hidden; 59 | z-index: 998; 60 | transition-property: all; 61 | transition-duration: .5s; 62 | } -------------------------------------------------------------------------------- /js/lib/epub.js/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, FuturePress 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | The views and conclusions contained in the software and documentation are those 26 | of the authors and should not be interpreted as representing official policies, 27 | either expressed or implied, of the FreeBSD Project. 28 | -------------------------------------------------------------------------------- /js/epub/locate.js: -------------------------------------------------------------------------------- 1 | var tocURLs = []; 2 | 3 | function getURLs(manifest) { 4 | var key; 5 | for (key in manifest) { 6 | if (!manifest.hasOwnProperty(key)) continue; 7 | if (typeof manifest[key] === 'function') continue; 8 | if (manifest[key] && manifest[key].href) tocURLs.push(manifest[key].href); 9 | } 10 | } 11 | 12 | // 修正页面竖直滚动模式下的页面内链接跳转问题 13 | function locate(e) { 14 | var book = window.parent.book, 15 | self = this, 16 | href, 17 | lastSeparator, 18 | anchor, 19 | item, 20 | i; 21 | 22 | href = self.href; 23 | lastSeparator = href.lastIndexOf('/'); 24 | href = href.substring(lastSeparator + 1, href.length); 25 | 26 | if (!href) 27 | return; 28 | 29 | if (href.indexOf('#') !== -1) { 30 | anchor = href.split('#')[1]; 31 | href = href.split('#')[0]; 32 | } 33 | 34 | for (i = 0; i < tocURLs.length; i++) { 35 | item = tocURLs[i]; 36 | if (href === 'reader.html') { // TODO 临时解决base标签失效的问题 37 | anchor ? window.parent.pageYScrollTo(anchor) : ''; 38 | return false; 39 | } 40 | if (item.indexOf(href) !== -1) { 41 | book.goto(item).then(function () { 42 | if (anchor) 43 | window.parent.pageYScrollTo(anchor); 44 | }); 45 | return false; 46 | } 47 | } 48 | } 49 | 50 | function bindEvent() { 51 | var a = document.getElementsByTagName('a'); 52 | 53 | getURLs(window.parent.book.manifest); 54 | 55 | a = Array.prototype.slice.call(a, 0); 56 | a.forEach(function (e) { 57 | e.onclick = locate; 58 | }); 59 | } 60 | 61 | bindEvent(); -------------------------------------------------------------------------------- /css/modules/button.css: -------------------------------------------------------------------------------- 1 | /* button */ 2 | 3 | [class*='btn-'] { 4 | display: inline-block; 5 | min-width: 4rem; 6 | border-width: 1px; 7 | border-style: solid; 8 | border-radius: .25em; 9 | padding: .4em 1.25em; 10 | text-align: center; 11 | cursor: pointer; 12 | transition: .3s; 13 | } 14 | 15 | [class*='btn-'].small { 16 | min-width: initial; 17 | } 18 | 19 | .btn-flat { 20 | color: white; 21 | } 22 | 23 | .btn-flat:hover { 24 | color: #1a1a1a; 25 | border-color: white; 26 | background-color: white; 27 | } 28 | 29 | .btn-prime { 30 | color: white; 31 | font-size: 14px; 32 | background-color: #1CBAA2; 33 | border-color: #1CBAA2; 34 | } 35 | 36 | .btn-prime:hover { 37 | box-shadow: 1px 2px 4px 0 rgba(0, 0, 0, .2); 38 | } 39 | 40 | .btn-tip { 41 | border: none; 42 | font-size: .875rem; 43 | } 44 | 45 | .btn-tip:hover { 46 | background-color: rgba(0, 0, 0, .2); 47 | } 48 | 49 | /* 滑动开关 */ 50 | 51 | .slidebar { 52 | position: relative; 53 | display: block; 54 | background-color: #828282; 55 | height: 15px; 56 | width: 40px; 57 | border-radius: 15px; 58 | cursor: pointer; 59 | transition: .3s; 60 | } 61 | 62 | .slidebar:after { 63 | content: ''; 64 | position: absolute; 65 | display: inline-block; 66 | top: 50%; 67 | left: -3px; 68 | width: 21px; 69 | height: 21px; 70 | border-radius: 50%; 71 | background-color: #e0e0e0; 72 | box-shadow: 0 1px 3px rgba(0, 0, 0, .4); 73 | transform: translateY(-50%); 74 | transition: .3s; 75 | } 76 | 77 | .slidebar.on { 78 | background-color: #F5C28A; 79 | } 80 | 81 | .slidebar.on:after { 82 | left: 22px; 83 | background-color: #F5AD8A; 84 | } -------------------------------------------------------------------------------- /js/epub/selection.js: -------------------------------------------------------------------------------- 1 | // 监听选中文本 2 | document.addEventListener('mouseup', function (e) { 3 | var sel = window.getSelection(); 4 | if (!sel.isCollapsed && window.parent.QiuSettings.popup) selected(e); 5 | else window.parent.document.getElementById('select-menu').style.visibility = 'hidden'; 6 | }); 7 | 8 | // 选中文本后的事件处理程序 9 | function selected(e) { 10 | var x = e.clientX, 11 | y = e.clientY, 12 | parentWin = window.parent, 13 | menu = parentWin.document.getElementById('select-menu'), 14 | tocSide = parentWin.document.getElementsByClassName('toc-side')[0], 15 | tocSideWidth = parentWin.document.defaultView.getComputedStyle(tocSide, null).width, 16 | menuHeight = parentWin.document.defaultView.getComputedStyle(menu, null).height, 17 | menuWidth = parentWin.document.defaultView.getComputedStyle(menu, null).width, 18 | screenHeight = parentWin.screen.availHeight, 19 | screenWidth = parentWin.screen.availWidth; 20 | 21 | x = parentWin.QiuSettings.sideToc ? x + parseInt(tocSideWidth) : x; 22 | x = parentWin.QiuSettings.pageMode ? x + parseInt(parentWin.QiuSettings.hLRMargin) : x; 23 | y = parentWin.QiuSettings.pageMode ? y + parseInt(parentWin.QiuSettings.hTBMargin) : y; 24 | 25 | menuHeight = parseInt(menuHeight); 26 | menuWidth = parseInt(menuWidth); 27 | 28 | if ((screenWidth - x) < menuWidth) { 29 | menu.style.left = x - menuWidth + 'px'; 30 | } else { 31 | menu.style.left = x + 'px' 32 | } 33 | if ((screenHeight - y) < menuHeight) { 34 | menu.style.top = y - menuHeight + 'px'; 35 | } else { 36 | menu.style.top = y + 'px' 37 | } 38 | 39 | menu.style.visibility = 'visible'; 40 | } 41 | 42 | // 解决复制粘贴问题的 hack 43 | document.addEventListener('keydown', function (e) { 44 | var key = e.keyCode || e.which; 45 | if (key === 67 && e.ctrlKey) { 46 | document.execCommand('copy', false, null); 47 | } 48 | }); -------------------------------------------------------------------------------- /css/font/icon-font.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Titillium Web'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Titillium Web Regular'), local('TitilliumWeb-Regular'), url(font.woff2) format('woff2'); 6 | } 7 | 8 | /* fallback */ 9 | @font-face { 10 | font-family: 'Material Icons'; 11 | font-style: normal; 12 | font-weight: 400; 13 | src: url(2fcrYFNaTjcS6g4U3t-Y5ZjZjT5FdEJ140U2DJYC3mY.woff2) format('woff2'); 14 | } 15 | 16 | .material-icons { 17 | font-family: 'Material Icons'; 18 | font-weight: normal; 19 | font-style: normal; 20 | font-size: 24px; 21 | line-height: 1; 22 | letter-spacing: normal; 23 | text-transform: none; 24 | display: inline-block; 25 | white-space: nowrap; 26 | word-wrap: normal; 27 | direction: ltr; 28 | -webkit-font-feature-settings: 'liga'; 29 | -webkit-font-smoothing: antialiased; 30 | } 31 | 32 | /* Inconsolata */ 33 | 34 | /* vietnamese */ 35 | @font-face { 36 | font-family: 'Inconsolata'; 37 | font-style: normal; 38 | font-weight: 400; 39 | src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url(QldKNThLqRwH-OJ1UHjlKGlW5qhWxg.woff2) format('woff2'); 40 | unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; 41 | } 42 | 43 | /* latin-ext */ 44 | @font-face { 45 | font-family: 'Inconsolata'; 46 | font-style: normal; 47 | font-weight: 400; 48 | src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url(QldKNThLqRwH-OJ1UHjlKGlX5qhWxg.woff2) format('woff2'); 49 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 50 | } 51 | 52 | /* latin */ 53 | @font-face { 54 | font-family: 'Inconsolata'; 55 | font-style: normal; 56 | font-weight: 400; 57 | src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url(QldKNThLqRwH-OJ1UHjlKGlZ5qg.woff2) format('woff2'); 58 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 59 | } -------------------------------------------------------------------------------- /js/highlight.js: -------------------------------------------------------------------------------- 1 | (function (win) { 2 | 3 | 'use strict'; 4 | 5 | var QiuPen = { 6 | highlighter: null 7 | }; 8 | 9 | var classes = [ 10 | 'hl-red', 'hl-orange', 'hl-yellow', 'hl-green', 'hl-blue', 'hl-purple', 11 | 'line-red', 'line-orange', 'line-yellow', 'line-green', 'line-blue', 'line-purple' 12 | ]; 13 | var classAppliers = []; 14 | 15 | function Highlight(chapterPos, highlight) { 16 | this.chapterPos = chapterPos; 17 | this.highlight = highlight; 18 | } 19 | 20 | QiuPen.init = function () { 21 | rangy.init(); // 初始化rangy模块 22 | classes.forEach(function (item) { 23 | var classApplier = rangy.createClassApplier(item, { 24 | ignoreWhiteSpace: true, 25 | elementTagName: 'span' 26 | }); 27 | classAppliers.push(classApplier); 28 | }); 29 | }; 30 | 31 | QiuPen.create = function (document) { 32 | QiuPen.highlighter = rangy.createHighlighter(document); // 创建一个highlighter 33 | classAppliers.forEach(function (item) { 34 | QiuPen.highlighter.addClassApplier(item); 35 | }); 36 | }; 37 | 38 | QiuPen.save = function (book) { 39 | var store = localStorage.getItem('highlight') || '{}'; 40 | store = JSON.parse(store); 41 | var bookKey = localStorage.getItem('reading'); 42 | store[bookKey] = store[bookKey] || []; 43 | var chapterPos = book.renderer.currentChapter.spinePos; 44 | var serStr = QiuPen.highlighter.serialize(); 45 | var hlObj = new Highlight(chapterPos, serStr); 46 | store[bookKey].push(hlObj); 47 | localStorage.setItem('highlight', JSON.stringify(store)); 48 | }; 49 | 50 | QiuPen.load = function (book) { 51 | var store = localStorage.getItem('highlight'); 52 | if (!store) return; 53 | var bookKey = localStorage.getItem('reading'); 54 | store = JSON.parse(store); 55 | var hlObjs = store[bookKey]; 56 | if (!hlObjs) return; 57 | var chapterPos = book.renderer.currentChapter.spinePos; 58 | var result = null; 59 | hlObjs.forEach(function (item) { 60 | if (item.chapterPos === chapterPos) { 61 | result = item; 62 | } 63 | }); 64 | if (!result) return; 65 | QiuPen.highlighter.deserialize(result.highlight); 66 | }; 67 | 68 | QiuPen.clear = function (bookKey) { 69 | var store = localStorage.getItem('highlight'); 70 | if (!store) return; 71 | store = JSON.parse(store); 72 | var hlObjs = store[bookKey]; 73 | if (!hlObjs) return; 74 | delete store[bookKey]; 75 | localStorage.setItem('highlight', JSON.stringify(store)); 76 | }; 77 | 78 | win.QiuPen = QiuPen; 79 | 80 | }(window)); -------------------------------------------------------------------------------- /js/bookDB.js: -------------------------------------------------------------------------------- 1 | (function (log, win) { 2 | 'use strict'; 3 | 4 | var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; 5 | var db; 6 | var bookDB = win.bookDB = {}; 7 | var isOpened = bookDB.isOpened = false; 8 | 9 | var request = indexedDB.open("books"); 10 | 11 | request.onupgradeneeded = function (e) { 12 | db = e.target.result; 13 | isOpened = true; 14 | db.createObjectStore('books', { 15 | keyPath: 'key' 16 | }); 17 | db.close(); 18 | }; 19 | 20 | var open = bookDB.open = function (success, error) { 21 | var request = indexedDB.open("books"); 22 | 23 | request.onsuccess = function (e) { 24 | db = e.target.result; 25 | isOpened = true; 26 | success(); 27 | }; 28 | 29 | request.onerror = error || function () { 30 | log('Can\'t open database'); 31 | }; 32 | }; 33 | 34 | var addBook = bookDB.addBook = function (book, success, error) { 35 | 36 | var t = db.transaction('books', 'readwrite'); 37 | var store = t.objectStore('books'); 38 | var req = store.put(book); 39 | 40 | req.onsuccess = success || null; 41 | req.onerror = error || null; 42 | }; 43 | 44 | var deleteBook = bookDB.deleteBook = function (key, success, error) { 45 | 46 | var t = db.transaction('books', 'readwrite'); 47 | var store = t.objectStore('books'); 48 | var req = store.delete(key); 49 | 50 | req.onsuccess = success || null; 51 | req.onerror = error || null; 52 | }; 53 | 54 | var getBook = bookDB.getBook = function (key, success, error) { 55 | 56 | var t = db.transaction('books', 'readonly'); 57 | var store = t.objectStore('books'); 58 | var req = store.get(key); 59 | 60 | if (success) { 61 | req.onsuccess = function (e) { 62 | success(e.target.result); 63 | }; 64 | } 65 | 66 | req.onerror = error || null; 67 | }; 68 | 69 | var getBooks = bookDB.getBooks = function (success, error) { 70 | var t = db.transaction('books', 'readonly'); 71 | var store = t.objectStore('books'); 72 | var req = store.openCursor(); 73 | var result = []; 74 | 75 | req.onsuccess = function (e) { 76 | var cursor = e.target.result; 77 | 78 | if (cursor) { 79 | result.push(cursor.value); 80 | cursor.continue(); 81 | } else { 82 | success(result); 83 | } 84 | }; 85 | 86 | req.onerror = error || null; 87 | }; 88 | 89 | var updateBook = bookDB.updateBook = function (book, success, error) { 90 | addBook(book, success, error); 91 | }; 92 | 93 | }(window.console.log, window)); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | QiuReader 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 35 |
36 | 37 |
38 | 44 |
45 | 46 |
47 |
48 | 49 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /js/bookMark.js: -------------------------------------------------------------------------------- 1 | // 记录书签信息 2 | // 以 book-marks 为键存储在 localStorage,值得 JSON 字符串 3 | // 序列化之前的对象以书籍的bookKey为属性,属性的值为MarkItem对象数组 4 | // MarkItem使用id为当前时间作为id 5 | 6 | (function (win) { 7 | 8 | 'use strict'; 9 | 10 | var marks = null, 11 | bookMark = {}; 12 | 13 | function MarkItem(cfi, posY) { 14 | this.id = new Date().getTime(); 15 | this.cfi = cfi; 16 | this.name = cfi; 17 | this.posY = posY; 18 | } 19 | 20 | // 添加书签 21 | var addBookMark = bookMark.addBookMark = function () { 22 | var key = localStorage.getItem('reading'); 23 | var markJSON = localStorage.getItem('book-marks'); 24 | var currentCfi = book.getCurrentLocationCfi(); 25 | var currentPosY = getYScroll(); 26 | var item = new MarkItem(currentCfi, currentPosY); 27 | 28 | if (markJSON) { 29 | marks = JSON.parse(markJSON); 30 | if (marks[key]) { 31 | marks[key].push(item); 32 | } 33 | else { 34 | marks[key] = []; 35 | marks[key].push(item); 36 | } 37 | } else { 38 | marks = {}; 39 | marks[key] = []; 40 | marks[key].push(item); 41 | } 42 | 43 | localStorage.setItem('book-marks', JSON.stringify(marks)); 44 | }; 45 | 46 | // 获取书签列表 47 | var getBookMarks = bookMark.getBookMarks = function () { 48 | var key = localStorage.getItem('reading'); 49 | var markJSON = localStorage.getItem('book-marks'); 50 | 51 | marks = markJSON ? JSON.parse(markJSON) : {}; 52 | marks[key] = marks[key] ? marks[key] : []; 53 | 54 | return marks[key]; 55 | }; 56 | 57 | // 删除某项书签 58 | var removeBookMark = bookMark.removeBookMark = function (markId) { 59 | var key = localStorage.getItem('reading'); 60 | var markJSON = localStorage.getItem('book-marks'); 61 | var markList; 62 | 63 | if (markJSON) { 64 | marks = JSON.parse(markJSON); 65 | marks = marks ? marks : {}; 66 | if (marks[key]) { 67 | markList = marks[key].filter(function (elem) { 68 | return (elem.id !== markId); 69 | }); 70 | marks[key] = markList; 71 | } else 72 | marks[key] = []; 73 | } else { 74 | marks = {}; 75 | marks[key] = []; 76 | } 77 | 78 | localStorage.setItem('book-marks', JSON.stringify(marks)); 79 | }; 80 | 81 | // 删除某本书的书签 82 | var removeBookMarks = bookMark.removeBookMarks = function (bookKey) { 83 | var markJSON = localStorage.getItem('book-marks'); 84 | 85 | if (markJSON) { 86 | marks = JSON.parse(markJSON); 87 | if (marks && marks[bookKey]) { 88 | delete marks[bookKey]; // 删除此属性 89 | localStorage.setItem('book-marks', JSON.stringify(marks)); 90 | } 91 | } 92 | }; 93 | 94 | // 修改书签内容 95 | var modifyBookMark = bookMark.modifyBookMark = function (markId, name) { 96 | var key = localStorage.getItem('reading'); 97 | var markJSON = localStorage.getItem('book-marks'); 98 | marks = JSON.parse(markJSON); 99 | 100 | marks[key].forEach(function (e, i, arr) { 101 | arr[i].name = e.id === markId ? name : arr[i].name; 102 | }); 103 | 104 | localStorage.setItem('book-marks', JSON.stringify(marks)); 105 | }; 106 | 107 | win.bookMark = bookMark; 108 | 109 | }(window)); 110 | -------------------------------------------------------------------------------- /js/modal.js: -------------------------------------------------------------------------------- 1 | 2 | (function (win) { 3 | 4 | 'use strict'; 5 | 6 | var QiuModal = win.QiuModal = {}; 7 | 8 | var property = { 9 | opacity: '.5', 10 | duration: '.5s', 11 | dismissible: false 12 | }; 13 | 14 | /** 15 | * 打开或关闭模态框的背景层 16 | */ 17 | var toggleOverlay = function (open) { 18 | var overlay = document.getElementsByClassName('modal-overlay')[0]; 19 | 20 | overlay.style.transitionDuration = property.duration; 21 | 22 | if (open) { 23 | overlay.style.visibility = 'visible'; 24 | overlay.style.opacity = property.opacity; 25 | } else { 26 | overlay.style.visibility = 'hidden'; 27 | overlay.style.opacity = '0'; 28 | } 29 | }; 30 | 31 | /** 32 | * 打开模态框 33 | */ 34 | var open = QiuModal.open = function (modalId) { 35 | var modal = document.getElementById(modalId); 36 | 37 | toggleOverlay(true); 38 | 39 | modal.style.transitionDuration = property.duration; 40 | 41 | modal.style.visibility = 'visible'; 42 | modal.style.opacity = '1'; 43 | modal.style.transform = 'scale(1, 1)'; 44 | }; 45 | 46 | /** 47 | * 关闭模态框 48 | */ 49 | var close = QiuModal.close = function (modalId) { 50 | var modal = document.getElementById(modalId); 51 | 52 | toggleOverlay(false); 53 | 54 | modal.style.transitionDuration = property.duration; 55 | 56 | modal.style.visibility = 'hidden'; 57 | modal.style.opacity = '0'; 58 | modal.style.transform = 'scale(0.7, 0.7)'; 59 | }; 60 | 61 | /** 62 | * 初始化,第一次使用模态框之前调用一次 63 | * 为 modal-trigger 和 modal-close 绑定事件处理程序 64 | */ 65 | var init = QiuModal.init = function () { 66 | var doc = win.document, 67 | trigger = doc.getElementsByClassName('modal-trigger'), 68 | closer = doc.getElementsByClassName('modal-close'), 69 | overlay, 70 | i; 71 | 72 | // 遍历绑定事件 73 | for (i = 0; i < trigger.length; i++) { 74 | trigger[i].onclick = function () { 75 | open(this.getAttribute('modal-target')); 76 | return false; 77 | }; 78 | } 79 | for (i = 0; i < closer.length; i++) { 80 | closer[i].onclick = function () { 81 | close(this.getAttribute('modal-target')); 82 | return false; 83 | }; 84 | } 85 | 86 | // 创建模态框的背景层:modal-overlay 87 | overlay = doc.createElement('div'); 88 | overlay.className = 'modal-overlay'; 89 | doc.getElementsByTagName('body')[0].appendChild(overlay); 90 | }; 91 | 92 | /** 93 | * 据传入的 option 来设置模态框参数,参数模仿 Materialize 的模态框部分设置 94 | * 95 | * option 对象的属性 96 | * 1. opacity:背景的透明度(默认为0.5) 97 | * 2. duration:出现的时间(默认为0.5s) 98 | * 3. dismissible:是否可以点击模态框外部退出(默认为false) 99 | */ 100 | var set = QiuModal.set = function (modalId, option) { 101 | var doc = win.document, 102 | modal, 103 | overlay; 104 | 105 | modal = doc.getElementById(modalId); 106 | overlay = doc.getElementsByClassName('modal-overlay')[0]; 107 | 108 | if (option && option.opacity) 109 | property.opacity = option.opacity; 110 | if (option && option.duration) 111 | property.duration = option.duration; 112 | if (option && option.top) 113 | property.top = option.top; 114 | if (option && option.dismissible) 115 | overlay.onclick = function () { 116 | close(modalId); 117 | }; 118 | }; 119 | 120 | }(window)); -------------------------------------------------------------------------------- /js/drag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 封装js拖拽对象 3 | * 4 | * 实现参考 https://codepen.io/yangbo5207/pen/LWjWpe 5 | * 对应教程 http://www.jianshu.com/p/b3dee0e84454 6 | * 7 | * 相较于教程,去掉不够灵活的transform,只使用绝对定位改变位置 8 | */ 9 | (function (win) { 10 | 11 | 'use strict'; 12 | 13 | // QiuDrag 的构造函数 14 | function QiuDrag(element) { 15 | this.element = typeof element === 'object' ? element : document.getElementById(element); 16 | 17 | // 鼠标初始位置 18 | this.startX = 0; 19 | this.startY = 0; 20 | // 目标元素初始位置 21 | this.sourceX = 0; 22 | this.sourceY = 0; 23 | 24 | this.init(); // 初始化 25 | } 26 | 27 | // 重写QiuDrag的原型,设置对象实例共享的方法 28 | QiuDrag.prototype = { 29 | constructor: QiuDrag, // 避免重写原型后丢失 constructor 属性 30 | 31 | init: function () { 32 | this.bindEvent.call(this); // 绑定事件处理程序 33 | }, 34 | 35 | getStyle: function (property) { 36 | return getStyle(this.element, property); 37 | }, 38 | 39 | getPosition: function () { 40 | var pos = {x: 0, y: 0}; 41 | 42 | if (this.getStyle('position') === 'static') { 43 | this.element.style.position = 'relative'; 44 | } else { 45 | pos = { 46 | x: parseInt(this.getStyle('left') ? this.getStyle('left') : 0), 47 | y: parseInt(this.getStyle('top') ? this.getStyle('top') : 0) 48 | } 49 | } 50 | 51 | return pos; 52 | }, 53 | 54 | setPosition: function (pos) { 55 | this.element.style.left = pos.x + 'px'; 56 | this.element.style.top = pos.y + 'px'; 57 | }, 58 | 59 | // 绑定事件处理程序 60 | bindEvent: function () { 61 | var self = this; 62 | self.element.addEventListener('mousedown', start, false); 63 | 64 | function start(event) { 65 | // 获取鼠标初始位置 66 | self.startX = event.pageX; 67 | self.startY = event.pageY; 68 | 69 | var pos = self.getPosition(); 70 | 71 | // 获取目标元素初始位置 72 | self.sourceX = pos.x; 73 | self.sourceY = pos.y; 74 | 75 | document.addEventListener('mousemove', move, false); 76 | document.addEventListener('mouseup', end, false); 77 | } 78 | 79 | function move(event) { 80 | // 获取鼠标当前位置 81 | var currentX = event.pageX; 82 | var currentY = event.pageY; 83 | 84 | // 鼠标移动距离 85 | var distanceX = currentX - self.startX; 86 | var distanceY = currentY - self.startY; 87 | 88 | var destX = (self.sourceX + distanceX).toFixed(); 89 | var destY = (self.sourceY + distanceY).toFixed(); 90 | 91 | // 元素可以设置的最大left和最大top(避免移出屏幕) 92 | var maxEX = win.innerWidth - parseFloat(self.getStyle('width')); 93 | var maxEY = win.innerHeight - parseFloat(self.getStyle('height')); 94 | 95 | destX = destX > maxEX ? maxEX : destX; 96 | destX = destX < 0 ? 0 : destX; 97 | destY = destY > maxEY ? maxEY : destY; 98 | destY = destY < 0 ? 0 : destY; 99 | 100 | // 计算并设置 101 | self.setPosition({ 102 | x: destX, 103 | y: destY 104 | }) 105 | } 106 | 107 | function end(event) { 108 | document.removeEventListener('mousemove', move); 109 | document.removeEventListener('mouseup', end); 110 | } 111 | } 112 | }; 113 | 114 | // 获取计算样式的兼容的写法 115 | function getStyle(e, property) { 116 | return document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(e, null)[property] : e.currentStyle[property]; 117 | } 118 | 119 | win.QiuDrag = QiuDrag; 120 | 121 | }(window)); -------------------------------------------------------------------------------- /js/setting.js: -------------------------------------------------------------------------------- 1 | // QiuSettings对象保存设置信息,以JSON字符串存储在localStorage(键为settings) 2 | (function (win) { 3 | 4 | 'use strict'; 5 | 6 | var QiuSettings = {}; 7 | 8 | QiuSettings.init = function () { 9 | var json = localStorage.getItem('settings'), 10 | o; 11 | 12 | if (json && JSON.parse(json)) { 13 | o = JSON.parse(json); 14 | QiuSettings.pageMode = o.pageMode; 15 | QiuSettings.fontSize = o.fontSize; 16 | QiuSettings.sideToc = o.sideToc; 17 | QiuSettings.lineHeight = o.lineHeight; 18 | QiuSettings.letterSpacing = o.letterSpacing; 19 | QiuSettings.wordSpacing = o.wordSpacing; 20 | QiuSettings.popup = o.popup; 21 | QiuSettings.hLRMargin = o.hLRMargin || '80px'; // horizontal 模式下的左右边距 22 | QiuSettings.hTBMargin = o.hTBMargin || '60px'; // horizontal 模式下的上下边距 23 | QiuSettings.vWidth = o.vWidth || '700px'; // vertical 模式下的页面宽度 24 | QiuSettings.forceSingle = !!o.forceSingle; // 是否启用强制单页 25 | } 26 | else { 27 | QiuSettings.pageMode = true; 28 | QiuSettings.fontSize = ''; 29 | QiuSettings.sideToc = true; 30 | QiuSettings.lineHeight = ''; 31 | QiuSettings.letterSpacing = ''; 32 | QiuSettings.wordSpacing = ''; 33 | QiuSettings.popup = true; 34 | QiuSettings.hLRMargin = '80px'; 35 | QiuSettings.hTBMargin = '60px'; 36 | QiuSettings.vWidth = '700px'; 37 | QiuSettings.forceSingle = false; 38 | localStorage.setItem('settings', JSON.stringify(QiuSettings)); 39 | } 40 | }; 41 | 42 | QiuSettings.setFontSize = function (size) { 43 | QiuSettings.fontSize = size; 44 | localStorage.setItem('settings', JSON.stringify(QiuSettings)); 45 | }; 46 | 47 | QiuSettings.setPageMode = function (mode) { 48 | QiuSettings.pageMode = mode; 49 | localStorage.setItem('settings', JSON.stringify(QiuSettings)); 50 | }; 51 | 52 | QiuSettings.setSideToc = function (visiable) { 53 | QiuSettings.sideToc = visiable; 54 | localStorage.setItem('settings', JSON.stringify(QiuSettings)); 55 | }; 56 | 57 | QiuSettings.setLineHeight = function (lineHeight) { 58 | QiuSettings.lineHeight = lineHeight; 59 | localStorage.setItem('settings', JSON.stringify(QiuSettings)); 60 | }; 61 | 62 | QiuSettings.setLetterSpacing = function (letterSpacing) { 63 | QiuSettings.letterSpacing = letterSpacing; 64 | localStorage.setItem('settings', JSON.stringify(QiuSettings)); 65 | }; 66 | 67 | QiuSettings.setWordSpacing = function (wordSpacing) { 68 | QiuSettings.wordSpacing = wordSpacing; 69 | localStorage.setItem('settings', JSON.stringify(QiuSettings)); 70 | }; 71 | 72 | QiuSettings.setPopup = function (popup) { 73 | QiuSettings.popup = popup; 74 | localStorage.setItem('settings', JSON.stringify(QiuSettings)); 75 | }; 76 | 77 | QiuSettings.setHLRMargin = function (margin) { 78 | QiuSettings.hLRMargin = margin; 79 | localStorage.setItem('settings', JSON.stringify(QiuSettings)); 80 | }; 81 | 82 | QiuSettings.setHTBMargin = function (margin) { 83 | QiuSettings.hTBMargin = margin; 84 | localStorage.setItem('settings', JSON.stringify(QiuSettings)); 85 | }; 86 | 87 | QiuSettings.setVWidth = function (margin) { 88 | QiuSettings.vWidth = margin; 89 | localStorage.setItem('settings', JSON.stringify(QiuSettings)); 90 | }; 91 | 92 | QiuSettings.setForceSingle = function (forceSingle) { 93 | QiuSettings.forceSingle = forceSingle; 94 | localStorage.setItem('settings', JSON.stringify(QiuSettings)); 95 | }; 96 | 97 | QiuSettings.resetStyle = function () { 98 | QiuSettings.fontSize = ''; 99 | QiuSettings.lineHeight = ''; 100 | QiuSettings.letterSpacing = ''; 101 | QiuSettings.wordSpacing = ''; 102 | QiuSettings.hLRMargin = '80px'; 103 | QiuSettings.hTBMargin = '60px'; 104 | QiuSettings.vWidth = '700px'; 105 | localStorage.setItem('settings', JSON.stringify(QiuSettings)); 106 | }; 107 | 108 | win.QiuSettings = QiuSettings; 109 | 110 | }(window)); -------------------------------------------------------------------------------- /css/pages/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | /* paper style */ 8 | 9 | html, body { 10 | height: 100%; 11 | background-color: #F2F4F5; 12 | font-family: sans-serif; 13 | } 14 | 15 | main { 16 | position: relative; 17 | padding-top: 80px; 18 | padding-left: 60px; 19 | padding-right: 60px; 20 | height: 100%; 21 | } 22 | 23 | .fixed-navbar { 24 | position: fixed; 25 | left: 0; 26 | top: 0; 27 | width: 100%; 28 | z-index: 100; 29 | } 30 | 31 | .file-btn { 32 | padding: 0.6rem 1rem; 33 | } 34 | 35 | .welcome { 36 | position: absolute; 37 | top: 50%; 38 | left: 50%; 39 | margin-top: -120px; 40 | margin-left: -225px; 41 | 42 | width: 500px; 43 | height: 250px; 44 | padding: 20px; 45 | font-family: 'Inconsolata', monospace; 46 | font-size: 1.25rem; 47 | text-align: center; 48 | background-color: #fff; 49 | box-shadow: 0 .5px 1px 0 rgba(0, 0, 0, .12); 50 | } 51 | 52 | .welcome img { 53 | width: 150px; 54 | height: 150px; 55 | animation: rotate 5s infinite linear; 56 | } 57 | 58 | @keyframes rotate { 59 | to { transform: rotate(360deg); } 60 | } 61 | 62 | .welcome-info > p { 63 | text-align: center; 64 | margin-bottom: 5px; 65 | } 66 | 67 | .book-list { 68 | overflow: auto; 69 | display: flex; 70 | flex-wrap: wrap; 71 | } 72 | 73 | .book { 74 | width: 260px; 75 | margin: 40px 20px 20px; 76 | /*float: left;*/ 77 | border-radius: 5px; 78 | overflow: hidden; 79 | background-color: #FFF; 80 | box-shadow: 0 2px 5px rgba(0, 0, 0, .2); 81 | transition: box-shadow .3s; 82 | } 83 | 84 | .book:hover { 85 | box-shadow: 0 2px 15px rgba(0, 0, 0, .2); 86 | } 87 | 88 | .book-head { 89 | position: relative; 90 | 91 | width: 260px; 92 | height: 160px; 93 | padding: 25px 35px; 94 | overflow: hidden; 95 | cursor: pointer; 96 | } 97 | 98 | .cover { 99 | position: relative; 100 | 101 | width: 190px; 102 | height: 110px; 103 | background-color: #FFF; 104 | } 105 | 106 | .cover-border { 107 | position: absolute; 108 | top: 0; 109 | bottom: 0; 110 | left: 0; 111 | right: 0; 112 | margin: auto; 113 | 114 | width: 150px; 115 | height: 70px; 116 | border: 2px solid #000; 117 | outline: 4px solid #000; 118 | outline-offset: 6px; 119 | } 120 | 121 | .book-title { 122 | position: absolute; 123 | left: 50%; 124 | top: 50%; 125 | transform: translate(-50%, -50%); 126 | width: inherit; 127 | height: inherit; 128 | text-align: center; 129 | display: flex; 130 | justify-content: center; 131 | align-items: center; 132 | overflow: hidden; 133 | } 134 | 135 | .book-body { 136 | padding: 10px; 137 | } 138 | 139 | .mask { 140 | position: absolute; 141 | top: 0; 142 | left: 0; 143 | 144 | width: 100%; 145 | height: 100%; 146 | visibility: hidden; 147 | z-index: 98; 148 | transition: all .3s; 149 | } 150 | 151 | .mask-content { 152 | position: absolute; 153 | top: 0; 154 | left: 0; 155 | 156 | width: 100%; 157 | height: 100%; 158 | padding: 15px; 159 | opacity: 0; 160 | z-index: 99; 161 | } 162 | 163 | .mask-content:hover { 164 | opacity: 1; 165 | } 166 | 167 | .mask-content:hover + .mask { 168 | visibility: visible; 169 | opacity: .6; 170 | background-color: #1a1a1a; 171 | } 172 | 173 | .mask-bottom { 174 | position: absolute; 175 | top: 50%; 176 | left: 50%; 177 | transform: translate(-50%, -50%); 178 | } 179 | 180 | .logo { 181 | font-family: 'Titillium Web', sans-serif; 182 | font-size: 1.5rem; 183 | } 184 | 185 | .navbar { 186 | width: 100%; 187 | height: 80px; 188 | background-color: white; 189 | box-shadow: 0 .5px 1px rgba(0, 0, 0, .15); 190 | } 191 | 192 | nav > ul { 193 | height: inherit; 194 | } 195 | 196 | nav > ul > li { 197 | float: left; 198 | display: flex; 199 | align-items: center; 200 | height: inherit; 201 | } 202 | 203 | .navbar-right { 204 | float: right; 205 | } 206 | 207 | .navbar-right input { 208 | display: none; 209 | } 210 | 211 | .modal { 212 | padding: 0; 213 | width: 40%; 214 | } 215 | 216 | .modal-header, 217 | .modal-content, 218 | .modal-footer 219 | { 220 | padding: 10px; 221 | } 222 | 223 | .modal-header { 224 | border-bottom: 1px solid #dedede; 225 | } 226 | 227 | .modal-footer { 228 | background-color: rgba(28, 186, 163, 0.15); 229 | } 230 | 231 | .modal-footer .btn-tip:hover { 232 | background-color: rgba(28, 186, 163, 0.2); 233 | } -------------------------------------------------------------------------------- /js/colorpicker.js: -------------------------------------------------------------------------------- 1 | // 参考:http://blog.csdn.net/d9hfdl73/article/details/55210228 2 | 3 | (function (win) { 4 | 5 | 'use strict'; 6 | 7 | var barWidth = 15; 8 | var panelWidth = 256; 9 | var margin = 5; 10 | var boxWidth = 15; 11 | var onChange; 12 | 13 | function QiuColorPicker(element, callback) { 14 | this.element = typeof element === 'object' ? element : document.getElementById(element); 15 | this.canvas = document.createElement('canvas'); 16 | this.canvas.width = barWidth + margin + panelWidth + margin + boxWidth; 17 | this.canvas.height = panelWidth; 18 | onChange = callback; 19 | this.init(); 20 | } 21 | 22 | QiuColorPicker.prototype = { 23 | 24 | constructor: QiuColorPicker, 25 | 26 | init: function () { 27 | this.element.appendChild(this.canvas); 28 | this.element.dataset.color = 'rgba(255,255,255,1)'; 29 | 30 | var canvas = this.canvas; 31 | paintBar(canvas, 0, 0, 0, panelWidth); 32 | paintPanel(canvas, 'rgba(255,0,0,1)', barWidth + margin, 0); 33 | paintBox(canvas, 'rgba(230,111,111,1)', canvas.width - boxWidth, 0); 34 | canvas.addEventListener('click', clickHandler); 35 | } 36 | 37 | }; 38 | 39 | // 默认为 15 * 256 的矩形 40 | function paintBar(canvas, x1, y1, x2, y2) { 41 | var context = canvas.getContext('2d'); 42 | var gradient = context.createLinearGradient(x1, y1, x2, y2); 43 | 44 | gradient.addColorStop(0, 'rgb(255,0,0)'); 45 | gradient.addColorStop(1 / 7, 'rgb(255,128,0)'); 46 | gradient.addColorStop(2 / 7, 'rgb(255,255,0)'); 47 | gradient.addColorStop(3 / 7, 'rgb(0,255,0)'); 48 | gradient.addColorStop(4 / 7, 'rgb(0,255,255)'); 49 | gradient.addColorStop(5 / 7, 'rgb(0,0,255)'); 50 | gradient.addColorStop(6 / 7, 'rgb(128,0,255)'); 51 | gradient.addColorStop(1, 'rgb(255,0,0)'); 52 | 53 | context.fillStyle = gradient; 54 | context.fillRect(x1, y1, barWidth, y2 - y1); 55 | } 56 | 57 | // 默认为 256 * 256 的矩形 58 | function paintPanel(canvas, color, x, y) { 59 | var context = canvas.getContext('2d'); 60 | var gradient = context.createLinearGradient(x, y, x + panelWidth, y); 61 | 62 | gradient.addColorStop(0, 'rgba(255,255,255,1)'); 63 | gradient.addColorStop(1, color); 64 | 65 | context.fillStyle = gradient; 66 | context.fillRect(x, y, panelWidth, panelWidth); 67 | 68 | gradient = context.createLinearGradient(x, y, x, y + panelWidth); 69 | gradient.addColorStop(0, 'rgba(0,0,0,0)'); 70 | gradient.addColorStop(1, 'rgba(0,0,0,1)'); 71 | 72 | context.fillStyle = gradient; 73 | context.fillRect(x, y, panelWidth, panelWidth); 74 | } 75 | 76 | // 已经选择的颜色 77 | function paintBox(canvas, color, x, y) { 78 | var context = canvas.getContext('2d'); 79 | context.fillStyle = color; 80 | context.fillRect(x, y, boxWidth, panelWidth); 81 | } 82 | 83 | // 响应点击的事件处理程序 84 | function clickHandler(event) { 85 | var pos = { 86 | x: event.offsetX || event.layerX, 87 | y: event.offsetY || event.layerY 88 | }; 89 | var canvas = event.target; 90 | if (pos.x >= 0 && pos.x <= barWidth && pos.y >= 0 && pos.y <= panelWidth) { 91 | canvas.dataset.color = getColorAtPoint(canvas, pos, 'bar'); 92 | paintPanel(canvas, canvas.dataset.color, barWidth + margin, 0); 93 | } else if (pos.x >= (barWidth + margin) && pos.x <= (barWidth + margin + panelWidth) && pos.y >= 0 && pos.y <= panelWidth) { 94 | canvas.dataset.color = getColorAtPoint(canvas, pos, 'panel'); 95 | } else { 96 | return; 97 | } 98 | console.log('color: ', canvas.dataset.color); 99 | paintBox(canvas, canvas.dataset.color, canvas.width - boxWidth, 0); 100 | typeof onChange === 'function' && onChange(canvas.dataset.color); 101 | } 102 | 103 | function getColorAtPoint(canvas, pos, area) { 104 | var context = canvas.getContext('2d'); 105 | var imgData; 106 | if (area === 'bar') { 107 | imgData = context.getImageData(0, 0, barWidth, panelWidth); 108 | } 109 | if (area === 'panel') { 110 | imgData = context.getImageData(barWidth + margin, 0, panelWidth, panelWidth); 111 | pos.x = pos.x - barWidth - margin; 112 | } 113 | var data = imgData.data; 114 | var dataIndex = (pos.y * imgData.width + pos.x) * 4; 115 | return 'rgba(' + 116 | data[dataIndex] + ',' + 117 | data[dataIndex + 1] + ',' + 118 | data[dataIndex + 2] + ',' + 119 | (data[dataIndex + 3] / 255).toFixed(2) + ')'; 120 | } 121 | 122 | win.QiuColorPicker = QiuColorPicker; 123 | 124 | }(window)); -------------------------------------------------------------------------------- /js/tab.js: -------------------------------------------------------------------------------- 1 | (function (win) { 2 | 3 | 'use strict'; 4 | 5 | // QiuTab 的构造函数 6 | function QiuTab(element) { 7 | this.element = typeof element === 'object' ? element : document.getElementById(element); 8 | this.tabs = _getTabs(this.element); 9 | this.panels = _getTargetPanels(this.element); 10 | this.slider = _addSlider(this.tabs); 11 | this.init(); 12 | } 13 | 14 | // 原型方法 15 | QiuTab.prototype = { 16 | constructor: QiuTab, 17 | 18 | init: function () { 19 | _initPanel(this.tabs, this.panels); // 初始化tab-panel 20 | _bindEvent(this); // 绑定事件 21 | }, 22 | 23 | // 选中特定tab 24 | select: function (tab_panel_id) { 25 | var targetTab, 26 | children = this.tabs.childNodes, 27 | targetPanel = document.getElementById(tab_panel_id), 28 | panels = this.panels, 29 | i; 30 | 31 | for (i = 0; i < children.length; i++) { 32 | if (children[i].nodeType === 1 && children[i].classList.contains('tab')) { 33 | if (children[i].firstElementChild && children[i].firstElementChild.href && children[i].firstElementChild.href.split('#')[1] === tab_panel_id) { 34 | targetTab = children[i]; 35 | targetTab.classList.add('active'); 36 | } else { 37 | children[i].classList.remove('active'); 38 | } 39 | } 40 | } 41 | 42 | for (i = 0; i < panels.length; i++) { 43 | if (panels[i].id !== tab_panel_id) 44 | panels[i].style.display = 'none'; 45 | } 46 | 47 | if (targetPanel && targetPanel.style) 48 | targetPanel.style.display = 'block'; 49 | 50 | // slider滑动到相应位置 51 | _setSliderPosition(this.tabs, this.slider); 52 | } 53 | }; 54 | 55 | function _getTabs(element) { 56 | var children = element.childNodes, 57 | tabs, 58 | i; 59 | 60 | for (i = 0; i < children.length; i++) { 61 | if (children[i].nodeType === 1 && children[i].classList.contains('tabs')) 62 | return children[i]; 63 | } 64 | } 65 | 66 | function _getTargetPanels(element) { 67 | var children = element.childNodes, 68 | panels = [], 69 | i; 70 | 71 | for (i = 0; i < children.length; i++) { 72 | if (children[i].nodeType === 1 && children[i].classList.contains('tab-panel')) 73 | panels.push(children[i]); 74 | } 75 | 76 | return panels; 77 | } 78 | 79 | function _addSlider(tabs) { 80 | var slider = document.createElement('div'); 81 | slider.className = 'tab-slider'; 82 | tabs.appendChild(slider); 83 | _setSliderPosition(tabs, slider); 84 | 85 | return slider; 86 | } 87 | 88 | function _setSliderPosition(tabs, slider) { 89 | var children = tabs.childNodes, 90 | left = 0, 91 | right = 0, 92 | i; 93 | 94 | for (i = 0; i < children.length; i++) { 95 | if (children[i].nodeType === 1 && children[i].classList.contains('tab')) 96 | right += parseFloat(_getWidth(children[i])); 97 | if (children[i].nodeType === 1 && children[i].classList.contains('active')) { 98 | left = right - parseFloat(_getWidth(children[i])); 99 | break; 100 | } 101 | } 102 | 103 | slider.style.left = left + 'px'; 104 | slider.style.right = parseFloat(_getWidth(tabs)) - right + 'px'; 105 | } 106 | 107 | function _getWidth(element) { 108 | return document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(element, null).width : element.currentStyle.width; 109 | } 110 | 111 | function _initPanel(tabs, panels) { 112 | var children = tabs.childNodes, 113 | activeTab, 114 | panelId, 115 | i; 116 | 117 | for (i = 0; i < children.length; i++) { 118 | if (children[i].classList && children[i].classList.contains('tab') && children[i].classList.contains('active')) 119 | activeTab = children[i]; 120 | } 121 | 122 | if (activeTab.firstElementChild && activeTab.firstElementChild.href) 123 | panelId = activeTab.firstElementChild.href.split('#')[1]; 124 | 125 | for (i = 0; i < panels.length; i++) { 126 | if (panels[i].id === panelId) 127 | panels[i].style.display = 'block'; 128 | else 129 | panels[i].style.display = 'none'; 130 | } 131 | } 132 | 133 | // 为tab绑定事件(事件委托) 134 | function _bindEvent(element) { 135 | element.tabs.addEventListener('click', function (e) { 136 | var target = e.target, 137 | targetId; 138 | 139 | if (target.href) { 140 | targetId = target.href.split('#')[1]; 141 | element.select(targetId); 142 | e.preventDefault(); 143 | } 144 | }); 145 | } 146 | 147 | win.QiuTab = QiuTab; 148 | 149 | }(window)); -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | var bookStr = '
{book-name}
Author:{author}
'; 2 | 3 | // 对应书籍列表页面的欢迎面板 4 | var welcome = { 5 | isVisible: true 6 | }; 7 | 8 | Object.defineProperty(welcome, 'visible', { 9 | set: function (val) { 10 | this.isVisible = val; 11 | var welcome = document.getElementsByClassName('welcome')[0]; 12 | welcome.className = val ? 'welcome' : 'welcome hidden'; 13 | } 14 | }); 15 | 16 | 17 | // 添加书籍按钮的事件处理程序 18 | document.getElementById('add-book') 19 | .addEventListener('change', function (e) { 20 | 21 | var firstFile = e.target.files[0]; 22 | 23 | if (window.FileReader) { 24 | var reader = new FileReader(); 25 | reader.readAsArrayBuffer(firstFile); 26 | 27 | reader.onload = function (e) { 28 | var book = ePub({ 29 | bookPath: e.target.result 30 | }); 31 | 32 | book.getMetadata().then(function (metadata) { 33 | var key = new Date().getTime() + '', 34 | name = metadata.bookTitle, 35 | author = metadata.creator, 36 | article = new Book(key, name, author, e.target.result); 37 | 38 | console.log(metadata); 39 | addBookToPage(article); 40 | bookDB.open(function () { 41 | bookDB.addBook( 42 | article, 43 | function () { 44 | console.log('add book successfully!'); 45 | }, 46 | function () { 47 | console.log('some error occured!'); 48 | } 49 | ); 50 | }); 51 | }); 52 | }.bind(this); 53 | 54 | reader.onerror = function () { 55 | alert('Σ(っ °Д °;)っ Some error occured, please try again!'); 56 | }; 57 | 58 | } else { 59 | alert('Your browser does not support the required features. Please use a modern browser such as Google Chrome, or Mozilla Firefox'); 60 | } 61 | }); 62 | 63 | // 编辑书籍信息按钮的事件处理程序(事件委托) 64 | document.getElementsByClassName('book-list')[0] 65 | .addEventListener('click', function (e) { 66 | var target = e.target, 67 | book, 68 | title = document.getElementById('book-title'), // title 输入框 69 | author = document.getElementById('book-author'), // author 输入框 70 | saveButton = document.getElementsByClassName('save-book-info')[0]; 71 | if (target && target.nodeName.toLocaleLowerCase() === 'i' && target.className.indexOf('edit') !== -1) { 72 | book = target 73 | .parentNode 74 | .parentNode 75 | .parentNode 76 | .parentNode; 77 | 78 | QiuModal.open('edit-book-info'); 79 | title.value = book.firstElementChild 80 | .firstElementChild 81 | .firstElementChild 82 | .firstElementChild 83 | .innerHTML; 84 | author.value = book.firstElementChild 85 | .nextElementSibling 86 | .firstElementChild 87 | .lastElementChild 88 | .innerHTML; 89 | saveButton.setAttribute('data-key', book.getAttribute('data-key')); 90 | } 91 | }); 92 | 93 | // 删除书籍的事件处理程序(事件委托) 94 | document.getElementsByClassName('book-list')[0] 95 | .addEventListener('click', function (e) { 96 | var target = e.target; 97 | var self = this; 98 | if (target && target.nodeName.toLocaleLowerCase() === 'i' && target.className.indexOf('delete') !== -1) { 99 | var book = target 100 | .parentNode 101 | .parentNode 102 | .parentNode 103 | .parentNode; 104 | 105 | bookDB.open( 106 | function () { 107 | var key = book.getAttribute('data-key'); 108 | bookDB.deleteBook( 109 | key, 110 | function () { 111 | self.removeChild(book); 112 | // 检查是否书籍列表是否为空,判断是否展示 welcome 面板 113 | welcome.visible = document.getElementsByClassName('book').length === 0; 114 | 115 | // 删除本地存储的阅读信息(进度、书签等) 116 | bookMark.removeBookMarks(key); // 删除书签记录 117 | currentLocation.clear(key); // 删除阅读进度信息 118 | QiuPen.clear(key); 119 | }, 120 | function () { 121 | alert('删除书籍失败,请刷新重试'); 122 | } 123 | ); 124 | } 125 | ); 126 | } 127 | }); 128 | 129 | // 打开书籍按钮的事件处理程序(事件委托) 130 | document.getElementsByClassName('book-list')[0] 131 | .addEventListener('click', function (e) { 132 | var target = e.target; 133 | if (target && target.nodeName.toLocaleLowerCase() === 'button' && target.className.indexOf('open-book') !== -1) { 134 | var book = target 135 | .parentNode 136 | .parentNode 137 | .parentNode 138 | .parentNode; 139 | localStorage.setItem('reading', book.getAttribute('data-key')); // 更改阅读信息的存储位置 140 | location.href = 'reader.html'; 141 | } 142 | }); 143 | 144 | // 编辑书籍信息模态框的Save按钮的事件处理程序 145 | document.getElementsByClassName('save-book-info')[0] 146 | .addEventListener('click', function (e) { 147 | var saveButton = e.target, 148 | title = document.getElementById('book-title'), // title 输入框 149 | author = document.getElementById('book-author'), // author 输入框 150 | bookList = document.getElementsByClassName('book-list')[0], 151 | key = saveButton.getAttribute('data-key'), 152 | book, 153 | i; 154 | 155 | for (i = 0; i < bookList.childNodes.length; i++) { 156 | if (bookList.childNodes[i].nodeType === 1 && bookList.childNodes[i].getAttribute('data-key') === key) { 157 | book = bookList.childNodes[i]; 158 | break; 159 | } 160 | } 161 | 162 | bookDB.open(function () { 163 | bookDB.getBook( 164 | key, 165 | function (result) { // result 为 indexedDB 中的 Book 对象 166 | result.name = title.value; 167 | result.author = author.value; 168 | 169 | // 更新书籍信息(这回调好变态啊!) 170 | bookDB.updateBook( 171 | result, 172 | function () { 173 | book.firstElementChild 174 | .firstElementChild 175 | .firstElementChild 176 | .firstElementChild 177 | .textContent = title.value; 178 | book.firstElementChild 179 | .nextElementSibling 180 | .firstElementChild 181 | .lastElementChild 182 | .textContent = author.value; 183 | console.log('更新书籍信息成功'); 184 | }, 185 | function () { 186 | alert('Can\'t update book, please try again.'); 187 | } 188 | ); 189 | 190 | }, 191 | function () { 192 | alert('保存信息失败,请刷新重试= ='); 193 | } 194 | ); 195 | }); 196 | }); 197 | 198 | // 打开页面时初始化书籍列表 199 | function init() { 200 | 'use strict'; 201 | 202 | bookDB.open(function () { 203 | bookDB.getBooks( 204 | function (books) { 205 | welcome.visible = books.length === 0; 206 | books.sort(function (a, b) { 207 | var keyA = parseInt(a.key), 208 | keyB = parseInt(b.key); 209 | return (keyA - keyB); 210 | }); 211 | books.forEach(function (book) { 212 | addBookToPage(book); 213 | }); 214 | }, 215 | function () { 216 | alert('获取书籍信息失败,请尝试刷新页面'); 217 | } 218 | ); 219 | }); 220 | } 221 | 222 | // 向页面添加一本书籍 223 | function addBookToPage(obj) { 224 | 'use strict'; 225 | 226 | var list = document.getElementsByClassName('book-list')[0], 227 | book = document.createElement('div'), 228 | bg, 229 | str; 230 | 231 | bg = 'bg-' + nextBG(); 232 | 233 | welcome.visible = false; // 确保关闭了 welcome 面板 234 | book.className = 'book'; 235 | book.setAttribute('data-key', obj.key); 236 | // 添加书籍名,其他信息 237 | str = bookStr.replace('{book-name}', obj.name) 238 | .replace('{author}', obj.author) 239 | .replace('{background}', bg); 240 | book.innerHTML = str; 241 | list.appendChild(book); 242 | } 243 | 244 | // 定义书籍的构造器 245 | function Book(key, name, author, content) { 246 | 'use strict'; 247 | 248 | this.key = key; 249 | this.name = name; 250 | this.author = author; 251 | this.content = content; 252 | } 253 | 254 | // 获取下一张背景图序号 255 | var nextBG = (function () { 256 | var number = 0; 257 | 258 | return function () { 259 | number += 1; 260 | number = (number % 5) ? number : 1; 261 | 262 | return number; 263 | } 264 | }()); 265 | 266 | window.onload = function () { 267 | init(); 268 | QiuModal.init(); // 初始化模态框 269 | }; -------------------------------------------------------------------------------- /js/lib/rangy-1.3.0/rangy-selectionsaverestore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Selection save and restore module for Rangy. 3 | * Saves and restores user selections using marker invisible elements in the DOM. 4 | * 5 | * Part of Rangy, a cross-browser JavaScript range and selection library 6 | * https://github.com/timdown/rangy 7 | * 8 | * Depends on Rangy core. 9 | * 10 | * Copyright 2015, Tim Down 11 | * Licensed under the MIT license. 12 | * Version: 1.3.0 13 | * Build date: 10 May 2015 14 | */ 15 | (function(factory, root) { 16 | if (typeof define == "function" && define.amd) { 17 | // AMD. Register as an anonymous module with a dependency on Rangy. 18 | define(["./rangy-core"], factory); 19 | } else if (typeof module != "undefined" && typeof exports == "object") { 20 | // Node/CommonJS style 21 | module.exports = factory( require("rangy") ); 22 | } else { 23 | // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 24 | factory(root.rangy); 25 | } 26 | })(function(rangy) { 27 | rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) { 28 | var dom = api.dom; 29 | var removeNode = dom.removeNode; 30 | var isDirectionBackward = api.Selection.isDirectionBackward; 31 | var markerTextChar = "\ufeff"; 32 | 33 | function gEBI(id, doc) { 34 | return (doc || document).getElementById(id); 35 | } 36 | 37 | function insertRangeBoundaryMarker(range, atStart) { 38 | var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2); 39 | var markerEl; 40 | var doc = dom.getDocument(range.startContainer); 41 | 42 | // Clone the Range and collapse to the appropriate boundary point 43 | var boundaryRange = range.cloneRange(); 44 | boundaryRange.collapse(atStart); 45 | 46 | // Create the marker element containing a single invisible character using DOM methods and insert it 47 | markerEl = doc.createElement("span"); 48 | markerEl.id = markerId; 49 | markerEl.style.lineHeight = "0"; 50 | markerEl.style.display = "none"; 51 | markerEl.className = "rangySelectionBoundary"; 52 | markerEl.appendChild(doc.createTextNode(markerTextChar)); 53 | 54 | boundaryRange.insertNode(markerEl); 55 | return markerEl; 56 | } 57 | 58 | function setRangeBoundary(doc, range, markerId, atStart) { 59 | var markerEl = gEBI(markerId, doc); 60 | if (markerEl) { 61 | range[atStart ? "setStartBefore" : "setEndBefore"](markerEl); 62 | removeNode(markerEl); 63 | } else { 64 | module.warn("Marker element has been removed. Cannot restore selection."); 65 | } 66 | } 67 | 68 | function compareRanges(r1, r2) { 69 | return r2.compareBoundaryPoints(r1.START_TO_START, r1); 70 | } 71 | 72 | function saveRange(range, direction) { 73 | var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString(); 74 | var backward = isDirectionBackward(direction); 75 | 76 | if (range.collapsed) { 77 | endEl = insertRangeBoundaryMarker(range, false); 78 | return { 79 | document: doc, 80 | markerId: endEl.id, 81 | collapsed: true 82 | }; 83 | } else { 84 | endEl = insertRangeBoundaryMarker(range, false); 85 | startEl = insertRangeBoundaryMarker(range, true); 86 | 87 | return { 88 | document: doc, 89 | startMarkerId: startEl.id, 90 | endMarkerId: endEl.id, 91 | collapsed: false, 92 | backward: backward, 93 | toString: function() { 94 | return "original text: '" + text + "', new text: '" + range.toString() + "'"; 95 | } 96 | }; 97 | } 98 | } 99 | 100 | function restoreRange(rangeInfo, normalize) { 101 | var doc = rangeInfo.document; 102 | if (typeof normalize == "undefined") { 103 | normalize = true; 104 | } 105 | var range = api.createRange(doc); 106 | if (rangeInfo.collapsed) { 107 | var markerEl = gEBI(rangeInfo.markerId, doc); 108 | if (markerEl) { 109 | markerEl.style.display = "inline"; 110 | var previousNode = markerEl.previousSibling; 111 | 112 | // Workaround for issue 17 113 | if (previousNode && previousNode.nodeType == 3) { 114 | removeNode(markerEl); 115 | range.collapseToPoint(previousNode, previousNode.length); 116 | } else { 117 | range.collapseBefore(markerEl); 118 | removeNode(markerEl); 119 | } 120 | } else { 121 | module.warn("Marker element has been removed. Cannot restore selection."); 122 | } 123 | } else { 124 | setRangeBoundary(doc, range, rangeInfo.startMarkerId, true); 125 | setRangeBoundary(doc, range, rangeInfo.endMarkerId, false); 126 | } 127 | 128 | if (normalize) { 129 | range.normalizeBoundaries(); 130 | } 131 | 132 | return range; 133 | } 134 | 135 | function saveRanges(ranges, direction) { 136 | var rangeInfos = [], range, doc; 137 | var backward = isDirectionBackward(direction); 138 | 139 | // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched 140 | ranges = ranges.slice(0); 141 | ranges.sort(compareRanges); 142 | 143 | for (var i = 0, len = ranges.length; i < len; ++i) { 144 | rangeInfos[i] = saveRange(ranges[i], backward); 145 | } 146 | 147 | // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie 148 | // between its markers 149 | for (i = len - 1; i >= 0; --i) { 150 | range = ranges[i]; 151 | doc = api.DomRange.getRangeDocument(range); 152 | if (range.collapsed) { 153 | range.collapseAfter(gEBI(rangeInfos[i].markerId, doc)); 154 | } else { 155 | range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc)); 156 | range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc)); 157 | } 158 | } 159 | 160 | return rangeInfos; 161 | } 162 | 163 | function saveSelection(win) { 164 | if (!api.isSelectionValid(win)) { 165 | module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus."); 166 | return null; 167 | } 168 | var sel = api.getSelection(win); 169 | var ranges = sel.getAllRanges(); 170 | var backward = (ranges.length == 1 && sel.isBackward()); 171 | 172 | var rangeInfos = saveRanges(ranges, backward); 173 | 174 | // Ensure current selection is unaffected 175 | if (backward) { 176 | sel.setSingleRange(ranges[0], backward); 177 | } else { 178 | sel.setRanges(ranges); 179 | } 180 | 181 | return { 182 | win: win, 183 | rangeInfos: rangeInfos, 184 | restored: false 185 | }; 186 | } 187 | 188 | function restoreRanges(rangeInfos) { 189 | var ranges = []; 190 | 191 | // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid 192 | // normalization affecting previously restored ranges. 193 | var rangeCount = rangeInfos.length; 194 | 195 | for (var i = rangeCount - 1; i >= 0; i--) { 196 | ranges[i] = restoreRange(rangeInfos[i], true); 197 | } 198 | 199 | return ranges; 200 | } 201 | 202 | function restoreSelection(savedSelection, preserveDirection) { 203 | if (!savedSelection.restored) { 204 | var rangeInfos = savedSelection.rangeInfos; 205 | var sel = api.getSelection(savedSelection.win); 206 | var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length; 207 | 208 | if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) { 209 | sel.removeAllRanges(); 210 | sel.addRange(ranges[0], true); 211 | } else { 212 | sel.setRanges(ranges); 213 | } 214 | 215 | savedSelection.restored = true; 216 | } 217 | } 218 | 219 | function removeMarkerElement(doc, markerId) { 220 | var markerEl = gEBI(markerId, doc); 221 | if (markerEl) { 222 | removeNode(markerEl); 223 | } 224 | } 225 | 226 | function removeMarkers(savedSelection) { 227 | var rangeInfos = savedSelection.rangeInfos; 228 | for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) { 229 | rangeInfo = rangeInfos[i]; 230 | if (rangeInfo.collapsed) { 231 | removeMarkerElement(savedSelection.doc, rangeInfo.markerId); 232 | } else { 233 | removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId); 234 | removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId); 235 | } 236 | } 237 | } 238 | 239 | api.util.extend(api, { 240 | saveRange: saveRange, 241 | restoreRange: restoreRange, 242 | saveRanges: saveRanges, 243 | restoreRanges: restoreRanges, 244 | saveSelection: saveSelection, 245 | restoreSelection: restoreSelection, 246 | removeMarkerElement: removeMarkerElement, 247 | removeMarkers: removeMarkers 248 | }); 249 | }); 250 | 251 | return rangy; 252 | }, this); -------------------------------------------------------------------------------- /user-stylesheet/light.css: -------------------------------------------------------------------------------- 1 | /* 需要电脑支持 emoji */ 2 | 3 | ::selection { 4 | background: rgba(133, 255, 159, 0.58) !important; 5 | color: #000 !important; 6 | } 7 | 8 | ::-moz-selection { 9 | background: rgba(133, 255, 159, 0.58) !important; 10 | color: #000 !important; 11 | } 12 | 13 | [class*=hl-] { 14 | color: #000 !important; 15 | } 16 | 17 | [class*=hl-]::before { 18 | content: '🍇'; 19 | } 20 | 21 | [class*=hl-]:hover { 22 | background-image: linear-gradient(0, rgba(0,0,0,.075), rgba(0,0,0,.075)); 23 | } 24 | 25 | .hl-red { 26 | background-color: rgba(255, 186, 159, 0.58) !important; 27 | } 28 | 29 | .hl-orange { 30 | background-color: rgba(255, 194, 103, 0.58) !important; 31 | } 32 | 33 | .hl-yellow { 34 | background-color: rgba(255, 249, 69, 0.58) !important; 35 | } 36 | 37 | .hl-green { 38 | background-color: rgba(154, 255, 99, 0.58) !important; 39 | } 40 | 41 | .hl-blue { 42 | background-color: rgba(101, 234, 255, 0.58) !important; 43 | } 44 | 45 | .hl-purple { 46 | background-color: rgba(185, 144, 255, 0.58) !important; 47 | } 48 | 49 | h1::before, 50 | h2::before, 51 | h3::before, 52 | h4::before, 53 | h5::before, 54 | h6::before { 55 | content: '🌿 '; 56 | } 57 | 58 | blockquote { 59 | padding-left: 1.5em; 60 | border-left: .3em solid #18841c; 61 | margin-bottom: 1.2em; 62 | } 63 | 64 | body { 65 | background: no-repeat fixed center calc(100% - 50px); 66 | } 67 | 68 | body { 69 | /* 图片来源:https://pixabay.com/en/tree-trunk-bark-green-woods-160469 */ 70 | background-image:url('data:image/png;base64,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'); 71 | } -------------------------------------------------------------------------------- /img/windmills.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /js/lib/rangy-1.3.0/rangy-serializer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serializer module for Rangy. 3 | * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a 4 | * cookie or local storage and restore it on the user's next visit to the same page. 5 | * 6 | * Part of Rangy, a cross-browser JavaScript range and selection library 7 | * https://github.com/timdown/rangy 8 | * 9 | * Depends on Rangy core. 10 | * 11 | * Copyright 2015, Tim Down 12 | * Licensed under the MIT license. 13 | * Version: 1.3.0 14 | * Build date: 10 May 2015 15 | */ 16 | (function(factory, root) { 17 | if (typeof define == "function" && define.amd) { 18 | // AMD. Register as an anonymous module with a dependency on Rangy. 19 | define(["./rangy-core"], factory); 20 | } else if (typeof module != "undefined" && typeof exports == "object") { 21 | // Node/CommonJS style 22 | module.exports = factory( require("rangy") ); 23 | } else { 24 | // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 25 | factory(root.rangy); 26 | } 27 | })(function(rangy) { 28 | rangy.createModule("Serializer", ["WrappedSelection"], function(api, module) { 29 | var UNDEF = "undefined"; 30 | var util = api.util; 31 | 32 | // encodeURIComponent and decodeURIComponent are required for cookie handling 33 | if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) { 34 | module.fail("encodeURIComponent and/or decodeURIComponent method is missing"); 35 | } 36 | 37 | // Checksum for checking whether range can be serialized 38 | var crc32 = (function() { 39 | function utf8encode(str) { 40 | var utf8CharCodes = []; 41 | 42 | for (var i = 0, len = str.length, c; i < len; ++i) { 43 | c = str.charCodeAt(i); 44 | if (c < 128) { 45 | utf8CharCodes.push(c); 46 | } else if (c < 2048) { 47 | utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128); 48 | } else { 49 | utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128); 50 | } 51 | } 52 | return utf8CharCodes; 53 | } 54 | 55 | var cachedCrcTable = null; 56 | 57 | function buildCRCTable() { 58 | var table = []; 59 | for (var i = 0, j, crc; i < 256; ++i) { 60 | crc = i; 61 | j = 8; 62 | while (j--) { 63 | if ((crc & 1) == 1) { 64 | crc = (crc >>> 1) ^ 0xEDB88320; 65 | } else { 66 | crc >>>= 1; 67 | } 68 | } 69 | table[i] = crc >>> 0; 70 | } 71 | return table; 72 | } 73 | 74 | function getCrcTable() { 75 | if (!cachedCrcTable) { 76 | cachedCrcTable = buildCRCTable(); 77 | } 78 | return cachedCrcTable; 79 | } 80 | 81 | return function(str) { 82 | var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable(); 83 | for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) { 84 | y = (crc ^ utf8CharCodes[i]) & 0xFF; 85 | crc = (crc >>> 8) ^ crcTable[y]; 86 | } 87 | return (crc ^ -1) >>> 0; 88 | }; 89 | })(); 90 | 91 | var dom = api.dom; 92 | 93 | function escapeTextForHtml(str) { 94 | return str.replace(//g, ">"); 95 | } 96 | 97 | function nodeToInfoString(node, infoParts) { 98 | infoParts = infoParts || []; 99 | var nodeType = node.nodeType, children = node.childNodes, childCount = children.length; 100 | var nodeInfo = [nodeType, node.nodeName, childCount].join(":"); 101 | var start = "", end = ""; 102 | switch (nodeType) { 103 | case 3: // Text node 104 | start = escapeTextForHtml(node.nodeValue); 105 | break; 106 | case 8: // Comment 107 | start = ""; 108 | break; 109 | default: 110 | start = "<" + nodeInfo + ">"; 111 | end = ""; 112 | break; 113 | } 114 | if (start) { 115 | infoParts.push(start); 116 | } 117 | for (var i = 0; i < childCount; ++i) { 118 | nodeToInfoString(children[i], infoParts); 119 | } 120 | if (end) { 121 | infoParts.push(end); 122 | } 123 | return infoParts; 124 | } 125 | 126 | // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all 127 | // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around 128 | // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's 129 | // innerHTML whenever the user changes an input within the element. 130 | function getElementChecksum(el) { 131 | var info = nodeToInfoString(el).join(""); 132 | return crc32(info).toString(16); 133 | } 134 | 135 | function serializePosition(node, offset, rootNode) { 136 | var pathParts = [], n = node; 137 | rootNode = rootNode || dom.getDocument(node).documentElement; 138 | while (n && n != rootNode) { 139 | pathParts.push(dom.getNodeIndex(n, true)); 140 | n = n.parentNode; 141 | } 142 | return pathParts.join("/") + ":" + offset; 143 | } 144 | 145 | function deserializePosition(serialized, rootNode, doc) { 146 | if (!rootNode) { 147 | rootNode = (doc || document).documentElement; 148 | } 149 | var parts = serialized.split(":"); 150 | var node = rootNode; 151 | var nodeIndices = parts[0] ? parts[0].split("/") : [], i = nodeIndices.length, nodeIndex; 152 | 153 | while (i--) { 154 | nodeIndex = parseInt(nodeIndices[i], 10); 155 | if (nodeIndex < node.childNodes.length) { 156 | node = node.childNodes[nodeIndex]; 157 | } else { 158 | throw module.createError("deserializePosition() failed: node " + dom.inspectNode(node) + 159 | " has no child with index " + nodeIndex + ", " + i); 160 | } 161 | } 162 | 163 | return new dom.DomPosition(node, parseInt(parts[1], 10)); 164 | } 165 | 166 | function serializeRange(range, omitChecksum, rootNode) { 167 | rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement; 168 | if (!dom.isOrIsAncestorOf(rootNode, range.commonAncestorContainer)) { 169 | throw module.createError("serializeRange(): range " + range.inspect() + 170 | " is not wholly contained within specified root node " + dom.inspectNode(rootNode)); 171 | } 172 | var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," + 173 | serializePosition(range.endContainer, range.endOffset, rootNode); 174 | if (!omitChecksum) { 175 | serialized += "{" + getElementChecksum(rootNode) + "}"; 176 | } 177 | return serialized; 178 | } 179 | 180 | var deserializeRegex = /^([^,]+),([^,\{]+)(\{([^}]+)\})?$/; 181 | 182 | function deserializeRange(serialized, rootNode, doc) { 183 | if (rootNode) { 184 | doc = doc || dom.getDocument(rootNode); 185 | } else { 186 | doc = doc || document; 187 | rootNode = doc.documentElement; 188 | } 189 | var result = deserializeRegex.exec(serialized); 190 | var checksum = result[4]; 191 | if (checksum) { 192 | var rootNodeChecksum = getElementChecksum(rootNode); 193 | if (checksum !== rootNodeChecksum) { 194 | throw module.createError("deserializeRange(): checksums of serialized range root node (" + checksum + 195 | ") and target root node (" + rootNodeChecksum + ") do not match"); 196 | } 197 | } 198 | var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc); 199 | var range = api.createRange(doc); 200 | range.setStartAndEnd(start.node, start.offset, end.node, end.offset); 201 | return range; 202 | } 203 | 204 | function canDeserializeRange(serialized, rootNode, doc) { 205 | if (!rootNode) { 206 | rootNode = (doc || document).documentElement; 207 | } 208 | var result = deserializeRegex.exec(serialized); 209 | var checksum = result[3]; 210 | return !checksum || checksum === getElementChecksum(rootNode); 211 | } 212 | 213 | function serializeSelection(selection, omitChecksum, rootNode) { 214 | selection = api.getSelection(selection); 215 | var ranges = selection.getAllRanges(), serializedRanges = []; 216 | for (var i = 0, len = ranges.length; i < len; ++i) { 217 | serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode); 218 | } 219 | return serializedRanges.join("|"); 220 | } 221 | 222 | function deserializeSelection(serialized, rootNode, win) { 223 | if (rootNode) { 224 | win = win || dom.getWindow(rootNode); 225 | } else { 226 | win = win || window; 227 | rootNode = win.document.documentElement; 228 | } 229 | var serializedRanges = serialized.split("|"); 230 | var sel = api.getSelection(win); 231 | var ranges = []; 232 | 233 | for (var i = 0, len = serializedRanges.length; i < len; ++i) { 234 | ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document); 235 | } 236 | sel.setRanges(ranges); 237 | 238 | return sel; 239 | } 240 | 241 | function canDeserializeSelection(serialized, rootNode, win) { 242 | var doc; 243 | if (rootNode) { 244 | doc = win ? win.document : dom.getDocument(rootNode); 245 | } else { 246 | win = win || window; 247 | rootNode = win.document.documentElement; 248 | } 249 | var serializedRanges = serialized.split("|"); 250 | 251 | for (var i = 0, len = serializedRanges.length; i < len; ++i) { 252 | if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) { 253 | return false; 254 | } 255 | } 256 | 257 | return true; 258 | } 259 | 260 | var cookieName = "rangySerializedSelection"; 261 | 262 | function getSerializedSelectionFromCookie(cookie) { 263 | var parts = cookie.split(/[;,]/); 264 | for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) { 265 | nameVal = parts[i].split("="); 266 | if (nameVal[0].replace(/^\s+/, "") == cookieName) { 267 | val = nameVal[1]; 268 | if (val) { 269 | return decodeURIComponent(val.replace(/\s+$/, "")); 270 | } 271 | } 272 | } 273 | return null; 274 | } 275 | 276 | function restoreSelectionFromCookie(win) { 277 | win = win || window; 278 | var serialized = getSerializedSelectionFromCookie(win.document.cookie); 279 | if (serialized) { 280 | deserializeSelection(serialized, win.doc); 281 | } 282 | } 283 | 284 | function saveSelectionCookie(win, props) { 285 | win = win || window; 286 | props = (typeof props == "object") ? props : {}; 287 | var expires = props.expires ? ";expires=" + props.expires.toUTCString() : ""; 288 | var path = props.path ? ";path=" + props.path : ""; 289 | var domain = props.domain ? ";domain=" + props.domain : ""; 290 | var secure = props.secure ? ";secure" : ""; 291 | var serialized = serializeSelection(api.getSelection(win)); 292 | win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure; 293 | } 294 | 295 | util.extend(api, { 296 | serializePosition: serializePosition, 297 | deserializePosition: deserializePosition, 298 | serializeRange: serializeRange, 299 | deserializeRange: deserializeRange, 300 | canDeserializeRange: canDeserializeRange, 301 | serializeSelection: serializeSelection, 302 | deserializeSelection: deserializeSelection, 303 | canDeserializeSelection: canDeserializeSelection, 304 | restoreSelectionFromCookie: restoreSelectionFromCookie, 305 | saveSelectionCookie: saveSelectionCookie, 306 | getElementChecksum: getElementChecksum, 307 | nodeToInfoString: nodeToInfoString 308 | }); 309 | 310 | util.crc32 = crc32; 311 | }); 312 | 313 | return rangy; 314 | }, this); -------------------------------------------------------------------------------- /css/pages/reader.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html, body { 8 | position: relative; 9 | height: 100%; 10 | background-color: #F2F4F5; 11 | overflow: hidden; 12 | transition: background-color .5s; 13 | } 14 | 15 | main { 16 | width: 100%; 17 | height: 100%; 18 | overflow: hidden; 19 | position: relative; 20 | } 21 | 22 | iframe { 23 | border: 0; 24 | } 25 | 26 | /* toc */ 27 | .toc-side { 28 | position: absolute; 29 | left: -300px; 30 | top: 0; 31 | width: 300px; 32 | height: 100%; 33 | background-color: #fff; 34 | overflow: auto; 35 | box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.14); 36 | } 37 | 38 | .with-animation .toc-side { 39 | transition: left .3s; 40 | } 41 | 42 | .toc-on .toc-side { 43 | left: 0; 44 | } 45 | 46 | .toc-side::-webkit-scrollbar { /* 滚动条 */ 47 | width: 8px; 48 | } 49 | 50 | .toc-side::-webkit-scrollbar-button { /* 滚动条两端的按钮 */ 51 | display: none; 52 | } 53 | 54 | .toc-side::-webkit-scrollbar-track { /* 外层轨道 */ 55 | background-color: #c7c9ca; 56 | } 57 | 58 | .toc-side::-webkit-scrollbar-thumb { /* 滚动块 */ 59 | background-color: #726E6B; 60 | border-radius: 8px; 61 | } 62 | 63 | .toc { 64 | height: 100.1%; /* 展示滚动条 */ 65 | } 66 | 67 | .toc-head { 68 | position: relative; 69 | width: 100%; 70 | padding: 20px; 71 | margin-bottom: 15px; 72 | text-align: center; 73 | font-size: 1.25rem; 74 | border-bottom: 1px dashed #000; 75 | } 76 | 77 | .chapter-list { 78 | padding-left: 10%; 79 | overflow: hidden; 80 | transition: .3s; 81 | } 82 | 83 | .chapter-list.collapse { 84 | /* 高度由js控制 */ 85 | } 86 | 87 | .chapter-list.expand { 88 | /* 高度由js控制 */ 89 | } 90 | 91 | .chapter-list-item { 92 | min-height: 20px; 93 | padding-top: 10px; 94 | padding-bottom: 10px; 95 | cursor: pointer; 96 | position: relative; 97 | } 98 | 99 | .chapter-list-item:not(:last-child):before { 100 | content: ''; 101 | display: block; 102 | position: absolute; 103 | left: 8px; 104 | height: 100%; 105 | width: 2px; 106 | background-color: #726E6B; 107 | } 108 | 109 | .item-content { 110 | display: flex; 111 | } 112 | 113 | .item-mark { 114 | position: relative; 115 | display: inline-block; 116 | width: 18px; 117 | height: 18px; 118 | border: 1px solid #e0e0e0; 119 | border-radius: 50%; 120 | margin-right: 1em; 121 | background: #e0e0e0; 122 | box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.14); 123 | flex-shrink: 0; /* 空间不足时不缩小 */ 124 | transition: .3s; 125 | } 126 | 127 | .item-mark:after { 128 | content: ''; 129 | display: block; 130 | position: absolute; 131 | top: 50%; 132 | right: -100%; 133 | width: 100%; 134 | height: 2px; 135 | background-color: #726E6B; 136 | } 137 | 138 | .item-mark:before { 139 | content: ''; 140 | display: block; 141 | position: absolute; 142 | left: 0; 143 | top: 0; 144 | width: 17px; 145 | height: 17px; 146 | transition: transform .3s; 147 | } 148 | 149 | .item-mark.expand:before { 150 | background: url("../../img/collapse.png") center no-repeat; 151 | transform: rotate(90deg); 152 | } 153 | 154 | .item-mark.collapse:before { 155 | background: url("../../img/collapse.png") center no-repeat; 156 | transform: rotate(0); 157 | } 158 | 159 | .item-mark:hover { 160 | border-color: #F5C28A; 161 | } 162 | 163 | .chapter-url { 164 | font-size: .875rem; 165 | transition: none; 166 | } 167 | 168 | .chapter-url:active { 169 | color: #f58f48; 170 | } 171 | 172 | /* 右侧 */ 173 | .main { 174 | position: absolute; 175 | left: 0; 176 | right: 0; 177 | top: 0; 178 | bottom: 0; 179 | } 180 | 181 | .with-animation .main { 182 | transition: left .3s; 183 | } 184 | 185 | .toc-on .main { 186 | left: 300px; 187 | } 188 | 189 | .page { 190 | position: absolute; 191 | left: 80px; 192 | right: 80px; 193 | top: 0; 194 | bottom: 0; 195 | margin: auto; 196 | } 197 | 198 | .page.doc { 199 | left: 0; 200 | right: 0; 201 | } 202 | 203 | .page.hide { 204 | display: none; 205 | } 206 | 207 | .prev-page, .next-page { 208 | position: absolute; 209 | display: flex; 210 | width: 80px; 211 | height: 30%; 212 | font-size: 30px; 213 | cursor: pointer; 214 | opacity: 0; 215 | transition: .5s; 216 | } 217 | 218 | .prev-page:hover, .next-page:hover { 219 | opacity: 1; 220 | } 221 | 222 | .prev-page { 223 | left: 0; 224 | top: 50%; 225 | background: url("../../img/left-arrow.png") no-repeat center; 226 | transform: translateY(-50%); 227 | } 228 | 229 | .next-page { 230 | right: 0; 231 | top: 50%; 232 | background: url("../../img/right-arrow.png") no-repeat center; 233 | transform: translateY(-50%); 234 | } 235 | 236 | /* svg toc-button */ 237 | 238 | .toc-button { 239 | position: absolute; 240 | top: 20px; 241 | left: 20px; 242 | cursor: pointer; 243 | z-index: 10; 244 | } 245 | 246 | /* 重置 modal 的背景色 */ 247 | .modal { 248 | background-color: #e3e3e3; 249 | } 250 | 251 | /* 重置 modal 的关闭按钮 */ 252 | .modal-close { 253 | border: 1px solid #444; 254 | } 255 | 256 | /* tool-bar */ 257 | .tool-bar { 258 | position: absolute; 259 | right: 0; /* todo 位置应与样式分离 */ 260 | bottom: 40px; 261 | width: 300px; /* todo 未指定width的bug*/ 262 | height: 40px; 263 | padding: 0 10px; 264 | border-radius: 4px; 265 | cursor: url("../../img/cursor-move.png"), auto; 266 | font-size: 0; /* hack: 去除inline-block间的空格 */ 267 | background: #ECE9E6; /* fallback for old browsers */ 268 | background: -webkit-linear-gradient(to bottom, #FFFFFF, #ECE9E6); /* Chrome 10-25, Safari 5.1-6 */ 269 | background: linear-gradient(to bottom, #FFFFFF, #ECE9E6); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 270 | box-shadow: 0 3px 15px 1px rgba(0, 0, 0, .2); 271 | transition: opacity .5s, transform .5s; 272 | } 273 | 274 | .drag-me { 275 | cursor: url("../../img/cursor-move.png"), auto; 276 | } 277 | 278 | .tool-bar-btn { 279 | display: inline-block; 280 | height: inherit; 281 | width: 40px; 282 | padding-left: 8px; 283 | padding-right: 8px; 284 | } 285 | 286 | .tool-bar-btn:not(.drag-me):hover { 287 | background-color: rgba(0, 0, 0, .15); 288 | } 289 | 290 | /* color-panel */ 291 | #color-panel .modal-content { 292 | display: flex; 293 | justify-content: space-around; 294 | align-items: center; 295 | } 296 | 297 | .color-picker canvas { 298 | cursor: crosshair; 299 | } 300 | 301 | .color-row { 302 | display: flex; 303 | margin: 20px; 304 | } 305 | 306 | .color-list { 307 | display: flex; 308 | align-items: center; 309 | justify-content: space-around; 310 | width: 195px; 311 | height: 55px; 312 | border-radius: 5px; 313 | background-color: #fff; 314 | cursor: pointer; 315 | transition: .3s; 316 | } 317 | 318 | .color-list:hover { 319 | box-shadow: 0 0.5em 1.5em rgba(0,0,0,.15); 320 | } 321 | 322 | .color-item { 323 | width: 25px; 324 | height: 25px; 325 | border: 1px solid transparent; 326 | border-radius: 5px; 327 | transition: .3s; 328 | } 329 | 330 | .color-item.selected { 331 | border-color: #f5c18a; 332 | } 333 | 334 | .color-item:hover { 335 | border-color: #f5c18a; 336 | } 337 | 338 | .background .color-item:nth-child(1) { 339 | background-color: #f6ebd0; 340 | } 341 | 342 | .background .color-item:nth-child(2) { 343 | background-color: #f1f3f4; 344 | } 345 | 346 | .background .color-item:nth-child(3) { 347 | background-color: #f7f6f2; 348 | } 349 | 350 | .background .color-item:nth-child(4) { 351 | background-color: #666; 352 | } 353 | 354 | .background .color-item:nth-child(5) { 355 | background-color: #3a4; 356 | } 357 | 358 | .font .color-item:nth-child(1) { 359 | background-color: #000; 360 | } 361 | 362 | .font .color-item:nth-child(2) { 363 | background-color: #444; 364 | } 365 | 366 | .font .color-item:nth-child(3) { 367 | background-color: #d3d3d3; 368 | } 369 | 370 | .font .color-item:nth-child(4) { 371 | background-color: #e5e5e5; 372 | } 373 | 374 | .font .color-item:nth-child(5) { 375 | background-color: #3a4; 376 | } 377 | 378 | /* setting panel */ 379 | 380 | .tabs { 381 | width: 45%; 382 | height: 40px; 383 | } 384 | 385 | .tab-panel { 386 | text-align: center; 387 | } 388 | 389 | .tab-panel { 390 | padding: 30px 0 10px; 391 | } 392 | 393 | .font-size-control { 394 | height: 32px; 395 | width: 30%; 396 | margin: auto; 397 | display: flex; 398 | justify-content: space-between; 399 | } 400 | 401 | .font-size-control span { 402 | line-height: 32px; 403 | } 404 | 405 | .font-size-control button { 406 | padding: .25em; 407 | } 408 | 409 | .page-control { 410 | width: 40%; 411 | margin: auto; 412 | display: flex; 413 | justify-content: space-around; 414 | } 415 | 416 | .other-control { 417 | width: 60%; 418 | margin: auto; 419 | display: flex; 420 | justify-content: center; 421 | flex-wrap: wrap; 422 | } 423 | 424 | .page-control i { 425 | width: 24px; 426 | height: 24px; 427 | } 428 | 429 | .style-control { 430 | display: flex; 431 | justify-content: space-around; 432 | align-items: center; 433 | height: 50px; 434 | width: 40%; 435 | margin: auto; 436 | } 437 | 438 | .letter-spacing-box { 439 | position: relative; 440 | } 441 | 442 | .spacing-control > div { 443 | width: 60%; 444 | margin: auto; 445 | justify-content: space-around; 446 | display: flex; 447 | } 448 | 449 | .spacing-info { 450 | width: 55%; 451 | } 452 | 453 | .ctrl-btns { 454 | width: 40%; 455 | } 456 | 457 | .ctrl-btns button { 458 | padding: .25em; 459 | display: table-cell; 460 | } 461 | 462 | .ctrl-btns i { 463 | vertical-align: middle; 464 | } 465 | 466 | .second-msg { 467 | font-size: 14px; 468 | color: gray; 469 | } 470 | 471 | .font-size, .line-height, .letter-spacing, .word-spacing { 472 | font-size: 14px; 473 | } 474 | 475 | .stylesheet, .restore { 476 | position: relative; 477 | width: 55px; 478 | height: 35px; 479 | color: #4d4d4d; 480 | border-radius: 5px; 481 | background-color: rgba(200, 200, 200, .5); 482 | cursor: pointer; 483 | transition: background-color .3s; 484 | } 485 | 486 | .stylesheet:hover, .restore:hover { 487 | background-color: #c8c8c8; 488 | } 489 | 490 | .stylesheet i, .restore i { 491 | position: absolute; 492 | top: 50%; 493 | left: 50%; 494 | transform: translate(-50%, -50%); 495 | } 496 | 497 | .style-info { 498 | margin-top: 20px; 499 | } 500 | 501 | .style-info a { 502 | color: #8A6BBE; 503 | text-decoration: none; 504 | } 505 | 506 | .style-info a:hover { 507 | color: #7b5b96; 508 | text-decoration: underline; 509 | } 510 | 511 | .other-row { 512 | width: 80%; 513 | display: flex; 514 | justify-content: center; 515 | align-items: center; 516 | } 517 | 518 | .other-row > span.second-msg { 519 | text-align: left; 520 | width: 70%; 521 | } 522 | 523 | .other-row > .ctrl-btns { 524 | text-align: right; 525 | width: 30%; 526 | } 527 | 528 | /* 书签栏 */ 529 | .book-mark-panel { 530 | position: fixed; 531 | left: 0; 532 | top: 0; 533 | height: 100vh; 534 | width: 300px; 535 | background-color: #fff; 536 | box-shadow: 2px 0 3px 0 rgba(0, 0, 0, .15); 537 | z-index: 999; 538 | transition: transform .3s; 539 | } 540 | 541 | .book-mark-overlay { 542 | content: ''; 543 | position: fixed; 544 | left: 0; 545 | top: 0; 546 | display: block; 547 | width: 100vw; 548 | height: 100vh; 549 | visibility: visible; 550 | opacity: .3; 551 | background-color: #000; 552 | z-index: 998; 553 | transition: .3s; 554 | } 555 | 556 | .book-mark-panel.hide { 557 | transform: translateX(-100%); 558 | } 559 | 560 | .book-mark-panel.hide + .book-mark-overlay { 561 | visibility: hidden; 562 | opacity: 0; 563 | } 564 | 565 | .book-mark-head { 566 | text-align: center; 567 | margin: 20px auto; 568 | font-size: 18px; 569 | } 570 | 571 | .book-mark-item { 572 | margin-bottom: 10px; 573 | border-bottom: 1px solid #dddddd; 574 | padding: 0 10px; 575 | } 576 | 577 | .book-mark-cfi { 578 | height: inherit; 579 | width: 210px; 580 | cursor: pointer; 581 | word-wrap: break-word; 582 | } 583 | 584 | .book-mark-cfi a { 585 | transition: none;; 586 | } 587 | 588 | .book-mark-cfi a:active { 589 | color: #f58f48; 590 | } 591 | 592 | .book-mark-control { 593 | height: inherit; 594 | width: 64px; 595 | cursor: pointer; 596 | } 597 | 598 | .book-mark-panel i { 599 | padding-left: 5px; 600 | cursor: pointer; 601 | transition: color .3s; 602 | } 603 | 604 | .book-mark-panel i:hover { 605 | color: #f5c18a; 606 | } 607 | 608 | .book-mark-panel-close { 609 | position: absolute; 610 | right: 20px; 611 | } 612 | 613 | /* 选中菜单 */ 614 | .select-menu { 615 | position: absolute; 616 | visibility: hidden; 617 | width: 200px; 618 | padding: 10px 0; 619 | background-color: #fff; 620 | box-shadow: 0 3px 12px rgba(0, 0, 0, .14); 621 | border: 1px solid rgba(27, 31, 35, 0.15); 622 | border-radius: 4px; 623 | z-index: 10; 624 | } 625 | 626 | .menu-item { 627 | padding: 4px 15px; 628 | cursor: pointer; 629 | transition: background-color .3s; 630 | } 631 | 632 | .menu-item:hover { 633 | background-color: rgba(220, 220, 220, .4); 634 | } 635 | 636 | .ann-color-bar { 637 | display: flex; 638 | justify-content: space-around; 639 | align-items: center; 640 | } 641 | 642 | .select-menu .divider { 643 | margin: 10px 0; 644 | } 645 | 646 | .ann-color { 647 | display: inline-block; 648 | width: 26px; 649 | height: 26px; 650 | border-radius: 50%; 651 | cursor: pointer; 652 | transition: transform .3s; 653 | } 654 | 655 | .ann-color:hover { 656 | transform: scale(1.2, 1.2); 657 | } 658 | 659 | .ann-underline-bar { 660 | display: flex; 661 | justify-content: space-around; 662 | align-items: center; 663 | } 664 | 665 | .ann-underline { 666 | display: inline-block; 667 | width: 22px; 668 | height: 22px; 669 | cursor: pointer; 670 | transition: transform .3s; 671 | } 672 | 673 | .ann-underline:hover { 674 | transform: scale(1.2, 1.2); 675 | } -------------------------------------------------------------------------------- /reader.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Qiu 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 |
23 |
24 |
Table of Contents
25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 |
33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 |
41 | 42 | 43 | 44 | 45 |
46 | 49 | 53 | 56 | 59 | 62 | 65 | 68 |
69 | 70 | 71 | 218 | 219 | 220 |
221 |
222 | Bookmarks 223 | close 224 |
225 | 227 |
228 |
229 | 230 | 231 | 246 | 247 | 248 | 293 | 294 | 295 |
296 |
297 | 298 | 299 | 300 | 301 | 302 |
303 |
304 | 305 | 306 |
307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | -------------------------------------------------------------------------------- /js/lib/rangy-1.3.0/rangy-highlighter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Highlighter module for Rangy, a cross-browser JavaScript range and selection library 3 | * https://github.com/timdown/rangy 4 | * 5 | * Depends on Rangy core, ClassApplier and optionally TextRange modules. 6 | * 7 | * Copyright 2015, Tim Down 8 | * Licensed under the MIT license. 9 | * Version: 1.3.0 10 | * Build date: 10 May 2015 11 | */ 12 | (function(factory, root) { 13 | if (typeof define == "function" && define.amd) { 14 | // AMD. Register as an anonymous module with a dependency on Rangy. 15 | define(["./rangy-core"], factory); 16 | } else if (typeof module != "undefined" && typeof exports == "object") { 17 | // Node/CommonJS style 18 | module.exports = factory( require("rangy") ); 19 | } else { 20 | // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) 21 | factory(root.rangy); 22 | } 23 | })(function(rangy) { 24 | rangy.createModule("Highlighter", ["ClassApplier"], function(api, module) { 25 | var dom = api.dom; 26 | var contains = dom.arrayContains; 27 | var getBody = dom.getBody; 28 | var createOptions = api.util.createOptions; 29 | var forEach = api.util.forEach; 30 | var nextHighlightId = 1; 31 | 32 | // Puts highlights in order, last in document first. 33 | function compareHighlights(h1, h2) { 34 | return h1.characterRange.start - h2.characterRange.start; 35 | } 36 | 37 | function getContainerElement(doc, id) { 38 | return id ? doc.getElementById(id) : getBody(doc); 39 | } 40 | 41 | /*----------------------------------------------------------------------------------------------------------------*/ 42 | 43 | var highlighterTypes = {}; 44 | 45 | function HighlighterType(type, converterCreator) { 46 | this.type = type; 47 | this.converterCreator = converterCreator; 48 | } 49 | 50 | HighlighterType.prototype.create = function() { 51 | var converter = this.converterCreator(); 52 | converter.type = this.type; 53 | return converter; 54 | }; 55 | 56 | function registerHighlighterType(type, converterCreator) { 57 | highlighterTypes[type] = new HighlighterType(type, converterCreator); 58 | } 59 | 60 | function getConverter(type) { 61 | var highlighterType = highlighterTypes[type]; 62 | if (highlighterType instanceof HighlighterType) { 63 | return highlighterType.create(); 64 | } else { 65 | throw new Error("Highlighter type '" + type + "' is not valid"); 66 | } 67 | } 68 | 69 | api.registerHighlighterType = registerHighlighterType; 70 | 71 | /*----------------------------------------------------------------------------------------------------------------*/ 72 | 73 | function CharacterRange(start, end) { 74 | this.start = start; 75 | this.end = end; 76 | } 77 | 78 | CharacterRange.prototype = { 79 | intersects: function(charRange) { 80 | return this.start < charRange.end && this.end > charRange.start; 81 | }, 82 | 83 | isContiguousWith: function(charRange) { 84 | return this.start == charRange.end || this.end == charRange.start; 85 | }, 86 | 87 | union: function(charRange) { 88 | return new CharacterRange(Math.min(this.start, charRange.start), Math.max(this.end, charRange.end)); 89 | }, 90 | 91 | intersection: function(charRange) { 92 | return new CharacterRange(Math.max(this.start, charRange.start), Math.min(this.end, charRange.end)); 93 | }, 94 | 95 | getComplements: function(charRange) { 96 | var ranges = []; 97 | if (this.start >= charRange.start) { 98 | if (this.end <= charRange.end) { 99 | return []; 100 | } 101 | ranges.push(new CharacterRange(charRange.end, this.end)); 102 | } else { 103 | ranges.push(new CharacterRange(this.start, Math.min(this.end, charRange.start))); 104 | if (this.end > charRange.end) { 105 | ranges.push(new CharacterRange(charRange.end, this.end)); 106 | } 107 | } 108 | return ranges; 109 | }, 110 | 111 | toString: function() { 112 | return "[CharacterRange(" + this.start + ", " + this.end + ")]"; 113 | } 114 | }; 115 | 116 | CharacterRange.fromCharacterRange = function(charRange) { 117 | return new CharacterRange(charRange.start, charRange.end); 118 | }; 119 | 120 | /*----------------------------------------------------------------------------------------------------------------*/ 121 | 122 | var textContentConverter = { 123 | rangeToCharacterRange: function(range, containerNode) { 124 | var bookmark = range.getBookmark(containerNode); 125 | return new CharacterRange(bookmark.start, bookmark.end); 126 | }, 127 | 128 | characterRangeToRange: function(doc, characterRange, containerNode) { 129 | var range = api.createRange(doc); 130 | range.moveToBookmark({ 131 | start: characterRange.start, 132 | end: characterRange.end, 133 | containerNode: containerNode 134 | }); 135 | 136 | return range; 137 | }, 138 | 139 | serializeSelection: function(selection, containerNode) { 140 | var ranges = selection.getAllRanges(), rangeCount = ranges.length; 141 | var rangeInfos = []; 142 | 143 | var backward = rangeCount == 1 && selection.isBackward(); 144 | 145 | for (var i = 0, len = ranges.length; i < len; ++i) { 146 | rangeInfos[i] = { 147 | characterRange: this.rangeToCharacterRange(ranges[i], containerNode), 148 | backward: backward 149 | }; 150 | } 151 | 152 | return rangeInfos; 153 | }, 154 | 155 | restoreSelection: function(selection, savedSelection, containerNode) { 156 | selection.removeAllRanges(); 157 | var doc = selection.win.document; 158 | for (var i = 0, len = savedSelection.length, range, rangeInfo, characterRange; i < len; ++i) { 159 | rangeInfo = savedSelection[i]; 160 | characterRange = rangeInfo.characterRange; 161 | range = this.characterRangeToRange(doc, rangeInfo.characterRange, containerNode); 162 | selection.addRange(range, rangeInfo.backward); 163 | } 164 | } 165 | }; 166 | 167 | registerHighlighterType("textContent", function() { 168 | return textContentConverter; 169 | }); 170 | 171 | /*----------------------------------------------------------------------------------------------------------------*/ 172 | 173 | // Lazily load the TextRange-based converter so that the dependency is only checked when required. 174 | registerHighlighterType("TextRange", (function() { 175 | var converter; 176 | 177 | return function() { 178 | if (!converter) { 179 | // Test that textRangeModule exists and is supported 180 | var textRangeModule = api.modules.TextRange; 181 | if (!textRangeModule) { 182 | throw new Error("TextRange module is missing."); 183 | } else if (!textRangeModule.supported) { 184 | throw new Error("TextRange module is present but not supported."); 185 | } 186 | 187 | converter = { 188 | rangeToCharacterRange: function(range, containerNode) { 189 | return CharacterRange.fromCharacterRange( range.toCharacterRange(containerNode) ); 190 | }, 191 | 192 | characterRangeToRange: function(doc, characterRange, containerNode) { 193 | var range = api.createRange(doc); 194 | range.selectCharacters(containerNode, characterRange.start, characterRange.end); 195 | return range; 196 | }, 197 | 198 | serializeSelection: function(selection, containerNode) { 199 | return selection.saveCharacterRanges(containerNode); 200 | }, 201 | 202 | restoreSelection: function(selection, savedSelection, containerNode) { 203 | selection.restoreCharacterRanges(containerNode, savedSelection); 204 | } 205 | }; 206 | } 207 | 208 | return converter; 209 | }; 210 | })()); 211 | 212 | /*----------------------------------------------------------------------------------------------------------------*/ 213 | 214 | function Highlight(doc, characterRange, classApplier, converter, id, containerElementId) { 215 | if (id) { 216 | this.id = id; 217 | nextHighlightId = Math.max(nextHighlightId, id + 1); 218 | } else { 219 | this.id = nextHighlightId++; 220 | } 221 | this.characterRange = characterRange; 222 | this.doc = doc; 223 | this.classApplier = classApplier; 224 | this.converter = converter; 225 | this.containerElementId = containerElementId || null; 226 | this.applied = false; 227 | } 228 | 229 | Highlight.prototype = { 230 | getContainerElement: function() { 231 | return getContainerElement(this.doc, this.containerElementId); 232 | }, 233 | 234 | getRange: function() { 235 | return this.converter.characterRangeToRange(this.doc, this.characterRange, this.getContainerElement()); 236 | }, 237 | 238 | fromRange: function(range) { 239 | this.characterRange = this.converter.rangeToCharacterRange(range, this.getContainerElement()); 240 | }, 241 | 242 | getText: function() { 243 | return this.getRange().toString(); 244 | }, 245 | 246 | containsElement: function(el) { 247 | return this.getRange().containsNodeContents(el.firstChild); 248 | }, 249 | 250 | unapply: function() { 251 | this.classApplier.undoToRange(this.getRange()); 252 | this.applied = false; 253 | }, 254 | 255 | apply: function() { 256 | this.classApplier.applyToRange(this.getRange()); 257 | this.applied = true; 258 | }, 259 | 260 | getHighlightElements: function() { 261 | return this.classApplier.getElementsWithClassIntersectingRange(this.getRange()); 262 | }, 263 | 264 | toString: function() { 265 | return "[Highlight(ID: " + this.id + ", class: " + this.classApplier.className + ", character range: " + 266 | this.characterRange.start + " - " + this.characterRange.end + ")]"; 267 | } 268 | }; 269 | 270 | /*----------------------------------------------------------------------------------------------------------------*/ 271 | 272 | function Highlighter(doc, type) { 273 | type = type || "textContent"; 274 | this.doc = doc || document; 275 | this.classAppliers = {}; 276 | this.highlights = []; 277 | this.converter = getConverter(type); 278 | } 279 | 280 | Highlighter.prototype = { 281 | addClassApplier: function(classApplier) { 282 | this.classAppliers[classApplier.className] = classApplier; 283 | }, 284 | 285 | getHighlightForElement: function(el) { 286 | var highlights = this.highlights; 287 | for (var i = 0, len = highlights.length; i < len; ++i) { 288 | if (highlights[i].containsElement(el)) { 289 | return highlights[i]; 290 | } 291 | } 292 | return null; 293 | }, 294 | 295 | removeHighlights: function(highlights) { 296 | for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) { 297 | highlight = this.highlights[i]; 298 | if (contains(highlights, highlight)) { 299 | highlight.unapply(); 300 | this.highlights.splice(i--, 1); 301 | } 302 | } 303 | }, 304 | 305 | removeAllHighlights: function() { 306 | this.removeHighlights(this.highlights); 307 | }, 308 | 309 | getIntersectingHighlights: function(ranges) { 310 | // Test each range against each of the highlighted ranges to see whether they overlap 311 | var intersectingHighlights = [], highlights = this.highlights; 312 | forEach(ranges, function(range) { 313 | //var selCharRange = converter.rangeToCharacterRange(range); 314 | forEach(highlights, function(highlight) { 315 | if (range.intersectsRange( highlight.getRange() ) && !contains(intersectingHighlights, highlight)) { 316 | intersectingHighlights.push(highlight); 317 | } 318 | }); 319 | }); 320 | 321 | return intersectingHighlights; 322 | }, 323 | 324 | highlightCharacterRanges: function(className, charRanges, options) { 325 | var i, len, j; 326 | var highlights = this.highlights; 327 | var converter = this.converter; 328 | var doc = this.doc; 329 | var highlightsToRemove = []; 330 | var classApplier = className ? this.classAppliers[className] : null; 331 | 332 | options = createOptions(options, { 333 | containerElementId: null, 334 | exclusive: true 335 | }); 336 | 337 | var containerElementId = options.containerElementId; 338 | var exclusive = options.exclusive; 339 | 340 | var containerElement, containerElementRange, containerElementCharRange; 341 | if (containerElementId) { 342 | containerElement = this.doc.getElementById(containerElementId); 343 | if (containerElement) { 344 | containerElementRange = api.createRange(this.doc); 345 | containerElementRange.selectNodeContents(containerElement); 346 | containerElementCharRange = new CharacterRange(0, containerElementRange.toString().length); 347 | } 348 | } 349 | 350 | var charRange, highlightCharRange, removeHighlight, isSameClassApplier, highlightsToKeep, splitHighlight; 351 | 352 | for (i = 0, len = charRanges.length; i < len; ++i) { 353 | charRange = charRanges[i]; 354 | highlightsToKeep = []; 355 | 356 | // Restrict character range to container element, if it exists 357 | if (containerElementCharRange) { 358 | charRange = charRange.intersection(containerElementCharRange); 359 | } 360 | 361 | // Ignore empty ranges 362 | if (charRange.start == charRange.end) { 363 | continue; 364 | } 365 | 366 | // Check for intersection with existing highlights. For each intersection, create a new highlight 367 | // which is the union of the highlight range and the selected range 368 | for (j = 0; j < highlights.length; ++j) { 369 | removeHighlight = false; 370 | 371 | if (containerElementId == highlights[j].containerElementId) { 372 | highlightCharRange = highlights[j].characterRange; 373 | isSameClassApplier = (classApplier == highlights[j].classApplier); 374 | splitHighlight = !isSameClassApplier && exclusive; 375 | 376 | // Replace the existing highlight if it needs to be: 377 | // 1. merged (isSameClassApplier) 378 | // 2. partially or entirely erased (className === null) 379 | // 3. partially or entirely replaced (isSameClassApplier == false && exclusive == true) 380 | if ( (highlightCharRange.intersects(charRange) || highlightCharRange.isContiguousWith(charRange)) && 381 | (isSameClassApplier || splitHighlight) ) { 382 | 383 | // Remove existing highlights, keeping the unselected parts 384 | if (splitHighlight) { 385 | forEach(highlightCharRange.getComplements(charRange), function(rangeToAdd) { 386 | highlightsToKeep.push( new Highlight(doc, rangeToAdd, highlights[j].classApplier, converter, null, containerElementId) ); 387 | }); 388 | } 389 | 390 | removeHighlight = true; 391 | if (isSameClassApplier) { 392 | charRange = highlightCharRange.union(charRange); 393 | } 394 | } 395 | } 396 | 397 | if (removeHighlight) { 398 | highlightsToRemove.push(highlights[j]); 399 | highlights[j] = new Highlight(doc, highlightCharRange.union(charRange), classApplier, converter, null, containerElementId); 400 | } else { 401 | highlightsToKeep.push(highlights[j]); 402 | } 403 | } 404 | 405 | // Add new range 406 | if (classApplier) { 407 | highlightsToKeep.push(new Highlight(doc, charRange, classApplier, converter, null, containerElementId)); 408 | } 409 | this.highlights = highlights = highlightsToKeep; 410 | } 411 | 412 | // Remove the old highlights 413 | forEach(highlightsToRemove, function(highlightToRemove) { 414 | highlightToRemove.unapply(); 415 | }); 416 | 417 | // Apply new highlights 418 | var newHighlights = []; 419 | forEach(highlights, function(highlight) { 420 | if (!highlight.applied) { 421 | highlight.apply(); 422 | newHighlights.push(highlight); 423 | } 424 | }); 425 | 426 | return newHighlights; 427 | }, 428 | 429 | highlightRanges: function(className, ranges, options) { 430 | var selCharRanges = []; 431 | var converter = this.converter; 432 | 433 | options = createOptions(options, { 434 | containerElement: null, 435 | exclusive: true 436 | }); 437 | 438 | var containerElement = options.containerElement; 439 | var containerElementId = containerElement ? containerElement.id : null; 440 | var containerElementRange; 441 | if (containerElement) { 442 | containerElementRange = api.createRange(containerElement); 443 | containerElementRange.selectNodeContents(containerElement); 444 | } 445 | 446 | forEach(ranges, function(range) { 447 | var scopedRange = containerElement ? containerElementRange.intersection(range) : range; 448 | selCharRanges.push( converter.rangeToCharacterRange(scopedRange, containerElement || getBody(range.getDocument())) ); 449 | }); 450 | 451 | return this.highlightCharacterRanges(className, selCharRanges, { 452 | containerElementId: containerElementId, 453 | exclusive: options.exclusive 454 | }); 455 | }, 456 | 457 | highlightSelection: function(className, options) { 458 | var converter = this.converter; 459 | var classApplier = className ? this.classAppliers[className] : false; 460 | 461 | options = createOptions(options, { 462 | containerElementId: null, 463 | selection: api.getSelection(this.doc), 464 | exclusive: true 465 | }); 466 | 467 | var containerElementId = options.containerElementId; 468 | var exclusive = options.exclusive; 469 | var selection = options.selection; 470 | var doc = selection.win.document; 471 | var containerElement = getContainerElement(doc, containerElementId); 472 | 473 | if (!classApplier && className !== false) { 474 | throw new Error("No class applier found for class '" + className + "'"); 475 | } 476 | 477 | // Store the existing selection as character ranges 478 | var serializedSelection = converter.serializeSelection(selection, containerElement); 479 | 480 | // Create an array of selected character ranges 481 | var selCharRanges = []; 482 | forEach(serializedSelection, function(rangeInfo) { 483 | selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) ); 484 | }); 485 | 486 | var newHighlights = this.highlightCharacterRanges(className, selCharRanges, { 487 | containerElementId: containerElementId, 488 | exclusive: exclusive 489 | }); 490 | 491 | // Restore selection 492 | converter.restoreSelection(selection, serializedSelection, containerElement); 493 | 494 | return newHighlights; 495 | }, 496 | 497 | unhighlightSelection: function(selection) { 498 | selection = selection || api.getSelection(this.doc); 499 | var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() ); 500 | this.removeHighlights(intersectingHighlights); 501 | selection.removeAllRanges(); 502 | return intersectingHighlights; 503 | }, 504 | 505 | getHighlightsInSelection: function(selection) { 506 | selection = selection || api.getSelection(this.doc); 507 | return this.getIntersectingHighlights(selection.getAllRanges()); 508 | }, 509 | 510 | selectionOverlapsHighlight: function(selection) { 511 | return this.getHighlightsInSelection(selection).length > 0; 512 | }, 513 | 514 | serialize: function(options) { 515 | var highlighter = this; 516 | var highlights = highlighter.highlights; 517 | var serializedType, serializedHighlights, convertType, serializationConverter; 518 | 519 | highlights.sort(compareHighlights); 520 | options = createOptions(options, { 521 | serializeHighlightText: false, 522 | type: highlighter.converter.type 523 | }); 524 | 525 | serializedType = options.type; 526 | convertType = (serializedType != highlighter.converter.type); 527 | 528 | if (convertType) { 529 | serializationConverter = getConverter(serializedType); 530 | } 531 | 532 | serializedHighlights = ["type:" + serializedType]; 533 | 534 | forEach(highlights, function(highlight) { 535 | var characterRange = highlight.characterRange; 536 | var containerElement; 537 | 538 | // Convert to the current Highlighter's type, if different from the serialization type 539 | if (convertType) { 540 | containerElement = highlight.getContainerElement(); 541 | characterRange = serializationConverter.rangeToCharacterRange( 542 | highlighter.converter.characterRangeToRange(highlighter.doc, characterRange, containerElement), 543 | containerElement 544 | ); 545 | } 546 | 547 | var parts = [ 548 | characterRange.start, 549 | characterRange.end, 550 | highlight.id, 551 | highlight.classApplier.className, 552 | highlight.containerElementId 553 | ]; 554 | 555 | if (options.serializeHighlightText) { 556 | parts.push(highlight.getText()); 557 | } 558 | serializedHighlights.push( parts.join("$") ); 559 | }); 560 | 561 | return serializedHighlights.join("|"); 562 | }, 563 | 564 | deserialize: function(serialized) { 565 | var serializedHighlights = serialized.split("|"); 566 | var highlights = []; 567 | 568 | var firstHighlight = serializedHighlights[0]; 569 | var regexResult; 570 | var serializationType, serializationConverter, convertType = false; 571 | if ( firstHighlight && (regexResult = /^type:(\w+)$/.exec(firstHighlight)) ) { 572 | serializationType = regexResult[1]; 573 | if (serializationType != this.converter.type) { 574 | serializationConverter = getConverter(serializationType); 575 | convertType = true; 576 | } 577 | serializedHighlights.shift(); 578 | } else { 579 | throw new Error("Serialized highlights are invalid."); 580 | } 581 | 582 | var classApplier, highlight, characterRange, containerElementId, containerElement; 583 | 584 | for (var i = serializedHighlights.length, parts; i-- > 0; ) { 585 | parts = serializedHighlights[i].split("$"); 586 | characterRange = new CharacterRange(+parts[0], +parts[1]); 587 | containerElementId = parts[4] || null; 588 | 589 | // Convert to the current Highlighter's type, if different from the serialization type 590 | if (convertType) { 591 | containerElement = getContainerElement(this.doc, containerElementId); 592 | characterRange = this.converter.rangeToCharacterRange( 593 | serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement), 594 | containerElement 595 | ); 596 | } 597 | 598 | classApplier = this.classAppliers[ parts[3] ]; 599 | 600 | if (!classApplier) { 601 | throw new Error("No class applier found for class '" + parts[3] + "'"); 602 | } 603 | 604 | highlight = new Highlight(this.doc, characterRange, classApplier, this.converter, parseInt(parts[2]), containerElementId); 605 | highlight.apply(); 606 | highlights.push(highlight); 607 | } 608 | this.highlights = highlights; 609 | } 610 | }; 611 | 612 | api.Highlighter = Highlighter; 613 | 614 | api.createHighlighter = function(doc, rangeCharacterOffsetConverterType) { 615 | return new Highlighter(doc, rangeCharacterOffsetConverterType); 616 | }; 617 | }); 618 | 619 | return rangy; 620 | }, this); 621 | -------------------------------------------------------------------------------- /js/lib/JSZip/LICENSE.markdown: -------------------------------------------------------------------------------- 1 | JSZip is dual licensed. You may use it under the MIT license *or* the GPLv3 2 | license. 3 | 4 | The MIT License 5 | =============== 6 | 7 | Copyright (c) 2009-2016 Stuart Knightley, David Duponchel, Franz Buchinger, António Afonso 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | 27 | 28 | GPL version 3 29 | ============= 30 | 31 | GNU GENERAL PUBLIC LICENSE 32 | Version 3, 29 June 2007 33 | 34 | Copyright (C) 2007 Free Software Foundation, Inc. 35 | Everyone is permitted to copy and distribute verbatim copies 36 | of this license document, but changing it is not allowed. 37 | 38 | Preamble 39 | 40 | The GNU General Public License is a free, copyleft license for 41 | software and other kinds of works. 42 | 43 | The licenses for most software and other practical works are designed 44 | to take away your freedom to share and change the works. By contrast, 45 | the GNU General Public License is intended to guarantee your freedom to 46 | share and change all versions of a program--to make sure it remains free 47 | software for all its users. We, the Free Software Foundation, use the 48 | GNU General Public License for most of our software; it applies also to 49 | any other work released this way by its authors. You can apply it to 50 | your programs, too. 51 | 52 | When we speak of free software, we are referring to freedom, not 53 | price. Our General Public Licenses are designed to make sure that you 54 | have the freedom to distribute copies of free software (and charge for 55 | them if you wish), that you receive source code or can get it if you 56 | want it, that you can change the software or use pieces of it in new 57 | free programs, and that you know you can do these things. 58 | 59 | To protect your rights, we need to prevent others from denying you 60 | these rights or asking you to surrender the rights. Therefore, you have 61 | certain responsibilities if you distribute copies of the software, or if 62 | you modify it: responsibilities to respect the freedom of others. 63 | 64 | For example, if you distribute copies of such a program, whether 65 | gratis or for a fee, you must pass on to the recipients the same 66 | freedoms that you received. You must make sure that they, too, receive 67 | or can get the source code. And you must show them these terms so they 68 | know their rights. 69 | 70 | Developers that use the GNU GPL protect your rights with two steps: 71 | (1) assert copyright on the software, and (2) offer you this License 72 | giving you legal permission to copy, distribute and/or modify it. 73 | 74 | For the developers' and authors' protection, the GPL clearly explains 75 | that there is no warranty for this free software. For both users' and 76 | authors' sake, the GPL requires that modified versions be marked as 77 | changed, so that their problems will not be attributed erroneously to 78 | authors of previous versions. 79 | 80 | Some devices are designed to deny users access to install or run 81 | modified versions of the software inside them, although the manufacturer 82 | can do so. This is fundamentally incompatible with the aim of 83 | protecting users' freedom to change the software. The systematic 84 | pattern of such abuse occurs in the area of products for individuals to 85 | use, which is precisely where it is most unacceptable. Therefore, we 86 | have designed this version of the GPL to prohibit the practice for those 87 | products. If such problems arise substantially in other domains, we 88 | stand ready to extend this provision to those domains in future versions 89 | of the GPL, as needed to protect the freedom of users. 90 | 91 | Finally, every program is threatened constantly by software patents. 92 | States should not allow patents to restrict development and use of 93 | software on general-purpose computers, but in those that do, we wish to 94 | avoid the special danger that patents applied to a free program could 95 | make it effectively proprietary. To prevent this, the GPL assures that 96 | patents cannot be used to render the program non-free. 97 | 98 | The precise terms and conditions for copying, distribution and 99 | modification follow. 100 | 101 | TERMS AND CONDITIONS 102 | 103 | 0. Definitions. 104 | 105 | "This License" refers to version 3 of the GNU General Public License. 106 | 107 | "Copyright" also means copyright-like laws that apply to other kinds of 108 | works, such as semiconductor masks. 109 | 110 | "The Program" refers to any copyrightable work licensed under this 111 | License. Each licensee is addressed as "you". "Licensees" and 112 | "recipients" may be individuals or organizations. 113 | 114 | To "modify" a work means to copy from or adapt all or part of the work 115 | in a fashion requiring copyright permission, other than the making of an 116 | exact copy. The resulting work is called a "modified version" of the 117 | earlier work or a work "based on" the earlier work. 118 | 119 | A "covered work" means either the unmodified Program or a work based 120 | on the Program. 121 | 122 | To "propagate" a work means to do anything with it that, without 123 | permission, would make you directly or secondarily liable for 124 | infringement under applicable copyright law, except executing it on a 125 | computer or modifying a private copy. Propagation includes copying, 126 | distribution (with or without modification), making available to the 127 | public, and in some countries other activities as well. 128 | 129 | To "convey" a work means any kind of propagation that enables other 130 | parties to make or receive copies. Mere interaction with a user through 131 | a computer network, with no transfer of a copy, is not conveying. 132 | 133 | An interactive user interface displays "Appropriate Legal Notices" 134 | to the extent that it includes a convenient and prominently visible 135 | feature that (1) displays an appropriate copyright notice, and (2) 136 | tells the user that there is no warranty for the work (except to the 137 | extent that warranties are provided), that licensees may convey the 138 | work under this License, and how to view a copy of this License. If 139 | the interface presents a list of user commands or options, such as a 140 | menu, a prominent item in the list meets this criterion. 141 | 142 | 1. Source Code. 143 | 144 | The "source code" for a work means the preferred form of the work 145 | for making modifications to it. "Object code" means any non-source 146 | form of a work. 147 | 148 | A "Standard Interface" means an interface that either is an official 149 | standard defined by a recognized standards body, or, in the case of 150 | interfaces specified for a particular programming language, one that 151 | is widely used among developers working in that language. 152 | 153 | The "System Libraries" of an executable work include anything, other 154 | than the work as a whole, that (a) is included in the normal form of 155 | packaging a Major Component, but which is not part of that Major 156 | Component, and (b) serves only to enable use of the work with that 157 | Major Component, or to implement a Standard Interface for which an 158 | implementation is available to the public in source code form. A 159 | "Major Component", in this context, means a major essential component 160 | (kernel, window system, and so on) of the specific operating system 161 | (if any) on which the executable work runs, or a compiler used to 162 | produce the work, or an object code interpreter used to run it. 163 | 164 | The "Corresponding Source" for a work in object code form means all 165 | the source code needed to generate, install, and (for an executable 166 | work) run the object code and to modify the work, including scripts to 167 | control those activities. However, it does not include the work's 168 | System Libraries, or general-purpose tools or generally available free 169 | programs which are used unmodified in performing those activities but 170 | which are not part of the work. For example, Corresponding Source 171 | includes interface definition files associated with source files for 172 | the work, and the source code for shared libraries and dynamically 173 | linked subprograms that the work is specifically designed to require, 174 | such as by intimate data communication or control flow between those 175 | subprograms and other parts of the work. 176 | 177 | The Corresponding Source need not include anything that users 178 | can regenerate automatically from other parts of the Corresponding 179 | Source. 180 | 181 | The Corresponding Source for a work in source code form is that 182 | same work. 183 | 184 | 2. Basic Permissions. 185 | 186 | All rights granted under this License are granted for the term of 187 | copyright on the Program, and are irrevocable provided the stated 188 | conditions are met. This License explicitly affirms your unlimited 189 | permission to run the unmodified Program. The output from running a 190 | covered work is covered by this License only if the output, given its 191 | content, constitutes a covered work. This License acknowledges your 192 | rights of fair use or other equivalent, as provided by copyright law. 193 | 194 | You may make, run and propagate covered works that you do not 195 | convey, without conditions so long as your license otherwise remains 196 | in force. You may convey covered works to others for the sole purpose 197 | of having them make modifications exclusively for you, or provide you 198 | with facilities for running those works, provided that you comply with 199 | the terms of this License in conveying all material for which you do 200 | not control copyright. Those thus making or running the covered works 201 | for you must do so exclusively on your behalf, under your direction 202 | and control, on terms that prohibit them from making any copies of 203 | your copyrighted material outside their relationship with you. 204 | 205 | Conveying under any other circumstances is permitted solely under 206 | the conditions stated below. Sublicensing is not allowed; section 10 207 | makes it unnecessary. 208 | 209 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 210 | 211 | No covered work shall be deemed part of an effective technological 212 | measure under any applicable law fulfilling obligations under article 213 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 214 | similar laws prohibiting or restricting circumvention of such 215 | measures. 216 | 217 | When you convey a covered work, you waive any legal power to forbid 218 | circumvention of technological measures to the extent such circumvention 219 | is effected by exercising rights under this License with respect to 220 | the covered work, and you disclaim any intention to limit operation or 221 | modification of the work as a means of enforcing, against the work's 222 | users, your or third parties' legal rights to forbid circumvention of 223 | technological measures. 224 | 225 | 4. Conveying Verbatim Copies. 226 | 227 | You may convey verbatim copies of the Program's source code as you 228 | receive it, in any medium, provided that you conspicuously and 229 | appropriately publish on each copy an appropriate copyright notice; 230 | keep intact all notices stating that this License and any 231 | non-permissive terms added in accord with section 7 apply to the code; 232 | keep intact all notices of the absence of any warranty; and give all 233 | recipients a copy of this License along with the Program. 234 | 235 | You may charge any price or no price for each copy that you convey, 236 | and you may offer support or warranty protection for a fee. 237 | 238 | 5. Conveying Modified Source Versions. 239 | 240 | You may convey a work based on the Program, or the modifications to 241 | produce it from the Program, in the form of source code under the 242 | terms of section 4, provided that you also meet all of these conditions: 243 | 244 | a) The work must carry prominent notices stating that you modified 245 | it, and giving a relevant date. 246 | 247 | b) The work must carry prominent notices stating that it is 248 | released under this License and any conditions added under section 249 | 7. This requirement modifies the requirement in section 4 to 250 | "keep intact all notices". 251 | 252 | c) You must license the entire work, as a whole, under this 253 | License to anyone who comes into possession of a copy. This 254 | License will therefore apply, along with any applicable section 7 255 | additional terms, to the whole of the work, and all its parts, 256 | regardless of how they are packaged. This License gives no 257 | permission to license the work in any other way, but it does not 258 | invalidate such permission if you have separately received it. 259 | 260 | d) If the work has interactive user interfaces, each must display 261 | Appropriate Legal Notices; however, if the Program has interactive 262 | interfaces that do not display Appropriate Legal Notices, your 263 | work need not make them do so. 264 | 265 | A compilation of a covered work with other separate and independent 266 | works, which are not by their nature extensions of the covered work, 267 | and which are not combined with it such as to form a larger program, 268 | in or on a volume of a storage or distribution medium, is called an 269 | "aggregate" if the compilation and its resulting copyright are not 270 | used to limit the access or legal rights of the compilation's users 271 | beyond what the individual works permit. Inclusion of a covered work 272 | in an aggregate does not cause this License to apply to the other 273 | parts of the aggregate. 274 | 275 | 6. Conveying Non-Source Forms. 276 | 277 | You may convey a covered work in object code form under the terms 278 | of sections 4 and 5, provided that you also convey the 279 | machine-readable Corresponding Source under the terms of this License, 280 | in one of these ways: 281 | 282 | a) Convey the object code in, or embodied in, a physical product 283 | (including a physical distribution medium), accompanied by the 284 | Corresponding Source fixed on a durable physical medium 285 | customarily used for software interchange. 286 | 287 | b) Convey the object code in, or embodied in, a physical product 288 | (including a physical distribution medium), accompanied by a 289 | written offer, valid for at least three years and valid for as 290 | long as you offer spare parts or customer support for that product 291 | model, to give anyone who possesses the object code either (1) a 292 | copy of the Corresponding Source for all the software in the 293 | product that is covered by this License, on a durable physical 294 | medium customarily used for software interchange, for a price no 295 | more than your reasonable cost of physically performing this 296 | conveying of source, or (2) access to copy the 297 | Corresponding Source from a network server at no charge. 298 | 299 | c) Convey individual copies of the object code with a copy of the 300 | written offer to provide the Corresponding Source. This 301 | alternative is allowed only occasionally and noncommercially, and 302 | only if you received the object code with such an offer, in accord 303 | with subsection 6b. 304 | 305 | d) Convey the object code by offering access from a designated 306 | place (gratis or for a charge), and offer equivalent access to the 307 | Corresponding Source in the same way through the same place at no 308 | further charge. You need not require recipients to copy the 309 | Corresponding Source along with the object code. If the place to 310 | copy the object code is a network server, the Corresponding Source 311 | may be on a different server (operated by you or a third party) 312 | that supports equivalent copying facilities, provided you maintain 313 | clear directions next to the object code saying where to find the 314 | Corresponding Source. Regardless of what server hosts the 315 | Corresponding Source, you remain obligated to ensure that it is 316 | available for as long as needed to satisfy these requirements. 317 | 318 | e) Convey the object code using peer-to-peer transmission, provided 319 | you inform other peers where the object code and Corresponding 320 | Source of the work are being offered to the general public at no 321 | charge under subsection 6d. 322 | 323 | A separable portion of the object code, whose source code is excluded 324 | from the Corresponding Source as a System Library, need not be 325 | included in conveying the object code work. 326 | 327 | A "User Product" is either (1) a "consumer product", which means any 328 | tangible personal property which is normally used for personal, family, 329 | or household purposes, or (2) anything designed or sold for incorporation 330 | into a dwelling. In determining whether a product is a consumer product, 331 | doubtful cases shall be resolved in favor of coverage. For a particular 332 | product received by a particular user, "normally used" refers to a 333 | typical or common use of that class of product, regardless of the status 334 | of the particular user or of the way in which the particular user 335 | actually uses, or expects or is expected to use, the product. A product 336 | is a consumer product regardless of whether the product has substantial 337 | commercial, industrial or non-consumer uses, unless such uses represent 338 | the only significant mode of use of the product. 339 | 340 | "Installation Information" for a User Product means any methods, 341 | procedures, authorization keys, or other information required to install 342 | and execute modified versions of a covered work in that User Product from 343 | a modified version of its Corresponding Source. The information must 344 | suffice to ensure that the continued functioning of the modified object 345 | code is in no case prevented or interfered with solely because 346 | modification has been made. 347 | 348 | If you convey an object code work under this section in, or with, or 349 | specifically for use in, a User Product, and the conveying occurs as 350 | part of a transaction in which the right of possession and use of the 351 | User Product is transferred to the recipient in perpetuity or for a 352 | fixed term (regardless of how the transaction is characterized), the 353 | Corresponding Source conveyed under this section must be accompanied 354 | by the Installation Information. But this requirement does not apply 355 | if neither you nor any third party retains the ability to install 356 | modified object code on the User Product (for example, the work has 357 | been installed in ROM). 358 | 359 | The requirement to provide Installation Information does not include a 360 | requirement to continue to provide support service, warranty, or updates 361 | for a work that has been modified or installed by the recipient, or for 362 | the User Product in which it has been modified or installed. Access to a 363 | network may be denied when the modification itself materially and 364 | adversely affects the operation of the network or violates the rules and 365 | protocols for communication across the network. 366 | 367 | Corresponding Source conveyed, and Installation Information provided, 368 | in accord with this section must be in a format that is publicly 369 | documented (and with an implementation available to the public in 370 | source code form), and must require no special password or key for 371 | unpacking, reading or copying. 372 | 373 | 7. Additional Terms. 374 | 375 | "Additional permissions" are terms that supplement the terms of this 376 | License by making exceptions from one or more of its conditions. 377 | Additional permissions that are applicable to the entire Program shall 378 | be treated as though they were included in this License, to the extent 379 | that they are valid under applicable law. If additional permissions 380 | apply only to part of the Program, that part may be used separately 381 | under those permissions, but the entire Program remains governed by 382 | this License without regard to the additional permissions. 383 | 384 | When you convey a copy of a covered work, you may at your option 385 | remove any additional permissions from that copy, or from any part of 386 | it. (Additional permissions may be written to require their own 387 | removal in certain cases when you modify the work.) You may place 388 | additional permissions on material, added by you to a covered work, 389 | for which you have or can give appropriate copyright permission. 390 | 391 | Notwithstanding any other provision of this License, for material you 392 | add to a covered work, you may (if authorized by the copyright holders of 393 | that material) supplement the terms of this License with terms: 394 | 395 | a) Disclaiming warranty or limiting liability differently from the 396 | terms of sections 15 and 16 of this License; or 397 | 398 | b) Requiring preservation of specified reasonable legal notices or 399 | author attributions in that material or in the Appropriate Legal 400 | Notices displayed by works containing it; or 401 | 402 | c) Prohibiting misrepresentation of the origin of that material, or 403 | requiring that modified versions of such material be marked in 404 | reasonable ways as different from the original version; or 405 | 406 | d) Limiting the use for publicity purposes of names of licensors or 407 | authors of the material; or 408 | 409 | e) Declining to grant rights under trademark law for use of some 410 | trade names, trademarks, or service marks; or 411 | 412 | f) Requiring indemnification of licensors and authors of that 413 | material by anyone who conveys the material (or modified versions of 414 | it) with contractual assumptions of liability to the recipient, for 415 | any liability that these contractual assumptions directly impose on 416 | those licensors and authors. 417 | 418 | All other non-permissive additional terms are considered "further 419 | restrictions" within the meaning of section 10. If the Program as you 420 | received it, or any part of it, contains a notice stating that it is 421 | governed by this License along with a term that is a further 422 | restriction, you may remove that term. If a license document contains 423 | a further restriction but permits relicensing or conveying under this 424 | License, you may add to a covered work material governed by the terms 425 | of that license document, provided that the further restriction does 426 | not survive such relicensing or conveying. 427 | 428 | If you add terms to a covered work in accord with this section, you 429 | must place, in the relevant source files, a statement of the 430 | additional terms that apply to those files, or a notice indicating 431 | where to find the applicable terms. 432 | 433 | Additional terms, permissive or non-permissive, may be stated in the 434 | form of a separately written license, or stated as exceptions; 435 | the above requirements apply either way. 436 | 437 | 8. Termination. 438 | 439 | You may not propagate or modify a covered work except as expressly 440 | provided under this License. Any attempt otherwise to propagate or 441 | modify it is void, and will automatically terminate your rights under 442 | this License (including any patent licenses granted under the third 443 | paragraph of section 11). 444 | 445 | However, if you cease all violation of this License, then your 446 | license from a particular copyright holder is reinstated (a) 447 | provisionally, unless and until the copyright holder explicitly and 448 | finally terminates your license, and (b) permanently, if the copyright 449 | holder fails to notify you of the violation by some reasonable means 450 | prior to 60 days after the cessation. 451 | 452 | Moreover, your license from a particular copyright holder is 453 | reinstated permanently if the copyright holder notifies you of the 454 | violation by some reasonable means, this is the first time you have 455 | received notice of violation of this License (for any work) from that 456 | copyright holder, and you cure the violation prior to 30 days after 457 | your receipt of the notice. 458 | 459 | Termination of your rights under this section does not terminate the 460 | licenses of parties who have received copies or rights from you under 461 | this License. If your rights have been terminated and not permanently 462 | reinstated, you do not qualify to receive new licenses for the same 463 | material under section 10. 464 | 465 | 9. Acceptance Not Required for Having Copies. 466 | 467 | You are not required to accept this License in order to receive or 468 | run a copy of the Program. Ancillary propagation of a covered work 469 | occurring solely as a consequence of using peer-to-peer transmission 470 | to receive a copy likewise does not require acceptance. However, 471 | nothing other than this License grants you permission to propagate or 472 | modify any covered work. These actions infringe copyright if you do 473 | not accept this License. Therefore, by modifying or propagating a 474 | covered work, you indicate your acceptance of this License to do so. 475 | 476 | 10. Automatic Licensing of Downstream Recipients. 477 | 478 | Each time you convey a covered work, the recipient automatically 479 | receives a license from the original licensors, to run, modify and 480 | propagate that work, subject to this License. You are not responsible 481 | for enforcing compliance by third parties with this License. 482 | 483 | An "entity transaction" is a transaction transferring control of an 484 | organization, or substantially all assets of one, or subdividing an 485 | organization, or merging organizations. If propagation of a covered 486 | work results from an entity transaction, each party to that 487 | transaction who receives a copy of the work also receives whatever 488 | licenses to the work the party's predecessor in interest had or could 489 | give under the previous paragraph, plus a right to possession of the 490 | Corresponding Source of the work from the predecessor in interest, if 491 | the predecessor has it or can get it with reasonable efforts. 492 | 493 | You may not impose any further restrictions on the exercise of the 494 | rights granted or affirmed under this License. For example, you may 495 | not impose a license fee, royalty, or other charge for exercise of 496 | rights granted under this License, and you may not initiate litigation 497 | (including a cross-claim or counterclaim in a lawsuit) alleging that 498 | any patent claim is infringed by making, using, selling, offering for 499 | sale, or importing the Program or any portion of it. 500 | 501 | 11. Patents. 502 | 503 | A "contributor" is a copyright holder who authorizes use under this 504 | License of the Program or a work on which the Program is based. The 505 | work thus licensed is called the contributor's "contributor version". 506 | 507 | A contributor's "essential patent claims" are all patent claims 508 | owned or controlled by the contributor, whether already acquired or 509 | hereafter acquired, that would be infringed by some manner, permitted 510 | by this License, of making, using, or selling its contributor version, 511 | but do not include claims that would be infringed only as a 512 | consequence of further modification of the contributor version. For 513 | purposes of this definition, "control" includes the right to grant 514 | patent sublicenses in a manner consistent with the requirements of 515 | this License. 516 | 517 | Each contributor grants you a non-exclusive, worldwide, royalty-free 518 | patent license under the contributor's essential patent claims, to 519 | make, use, sell, offer for sale, import and otherwise run, modify and 520 | propagate the contents of its contributor version. 521 | 522 | In the following three paragraphs, a "patent license" is any express 523 | agreement or commitment, however denominated, not to enforce a patent 524 | (such as an express permission to practice a patent or covenant not to 525 | sue for patent infringement). To "grant" such a patent license to a 526 | party means to make such an agreement or commitment not to enforce a 527 | patent against the party. 528 | 529 | If you convey a covered work, knowingly relying on a patent license, 530 | and the Corresponding Source of the work is not available for anyone 531 | to copy, free of charge and under the terms of this License, through a 532 | publicly available network server or other readily accessible means, 533 | then you must either (1) cause the Corresponding Source to be so 534 | available, or (2) arrange to deprive yourself of the benefit of the 535 | patent license for this particular work, or (3) arrange, in a manner 536 | consistent with the requirements of this License, to extend the patent 537 | license to downstream recipients. "Knowingly relying" means you have 538 | actual knowledge that, but for the patent license, your conveying the 539 | covered work in a country, or your recipient's use of the covered work 540 | in a country, would infringe one or more identifiable patents in that 541 | country that you have reason to believe are valid. 542 | 543 | If, pursuant to or in connection with a single transaction or 544 | arrangement, you convey, or propagate by procuring conveyance of, a 545 | covered work, and grant a patent license to some of the parties 546 | receiving the covered work authorizing them to use, propagate, modify 547 | or convey a specific copy of the covered work, then the patent license 548 | you grant is automatically extended to all recipients of the covered 549 | work and works based on it. 550 | 551 | A patent license is "discriminatory" if it does not include within 552 | the scope of its coverage, prohibits the exercise of, or is 553 | conditioned on the non-exercise of one or more of the rights that are 554 | specifically granted under this License. You may not convey a covered 555 | work if you are a party to an arrangement with a third party that is 556 | in the business of distributing software, under which you make payment 557 | to the third party based on the extent of your activity of conveying 558 | the work, and under which the third party grants, to any of the 559 | parties who would receive the covered work from you, a discriminatory 560 | patent license (a) in connection with copies of the covered work 561 | conveyed by you (or copies made from those copies), or (b) primarily 562 | for and in connection with specific products or compilations that 563 | contain the covered work, unless you entered into that arrangement, 564 | or that patent license was granted, prior to 28 March 2007. 565 | 566 | Nothing in this License shall be construed as excluding or limiting 567 | any implied license or other defenses to infringement that may 568 | otherwise be available to you under applicable patent law. 569 | 570 | 12. No Surrender of Others' Freedom. 571 | 572 | If conditions are imposed on you (whether by court order, agreement or 573 | otherwise) that contradict the conditions of this License, they do not 574 | excuse you from the conditions of this License. If you cannot convey a 575 | covered work so as to satisfy simultaneously your obligations under this 576 | License and any other pertinent obligations, then as a consequence you may 577 | not convey it at all. For example, if you agree to terms that obligate you 578 | to collect a royalty for further conveying from those to whom you convey 579 | the Program, the only way you could satisfy both those terms and this 580 | License would be to refrain entirely from conveying the Program. 581 | 582 | 13. Use with the GNU Affero General Public License. 583 | 584 | Notwithstanding any other provision of this License, you have 585 | permission to link or combine any covered work with a work licensed 586 | under version 3 of the GNU Affero General Public License into a single 587 | combined work, and to convey the resulting work. The terms of this 588 | License will continue to apply to the part which is the covered work, 589 | but the special requirements of the GNU Affero General Public License, 590 | section 13, concerning interaction through a network will apply to the 591 | combination as such. 592 | 593 | 14. Revised Versions of this License. 594 | 595 | The Free Software Foundation may publish revised and/or new versions of 596 | the GNU General Public License from time to time. Such new versions will 597 | be similar in spirit to the present version, but may differ in detail to 598 | address new problems or concerns. 599 | 600 | Each version is given a distinguishing version number. If the 601 | Program specifies that a certain numbered version of the GNU General 602 | Public License "or any later version" applies to it, you have the 603 | option of following the terms and conditions either of that numbered 604 | version or of any later version published by the Free Software 605 | Foundation. If the Program does not specify a version number of the 606 | GNU General Public License, you may choose any version ever published 607 | by the Free Software Foundation. 608 | 609 | If the Program specifies that a proxy can decide which future 610 | versions of the GNU General Public License can be used, that proxy's 611 | public statement of acceptance of a version permanently authorizes you 612 | to choose that version for the Program. 613 | 614 | Later license versions may give you additional or different 615 | permissions. However, no additional obligations are imposed on any 616 | author or copyright holder as a result of your choosing to follow a 617 | later version. 618 | 619 | 15. Disclaimer of Warranty. 620 | 621 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 622 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 623 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 624 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 625 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 626 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 627 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 628 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 629 | 630 | 16. Limitation of Liability. 631 | 632 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 633 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 634 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 635 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 636 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 637 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 638 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 639 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 640 | SUCH DAMAGES. 641 | 642 | 17. Interpretation of Sections 15 and 16. 643 | 644 | If the disclaimer of warranty and limitation of liability provided 645 | above cannot be given local legal effect according to their terms, 646 | reviewing courts shall apply local law that most closely approximates 647 | an absolute waiver of all civil liability in connection with the 648 | Program, unless a warranty or assumption of liability accompanies a 649 | copy of the Program in return for a fee. 650 | 651 | END OF TERMS AND CONDITIONS 652 | --------------------------------------------------------------------------------