修改查看日志的展示方式

This commit is contained in:
mxd 2019-11-10 17:39:50 +08:00
parent c1b54b4b4e
commit 9e640ee7a8
5 changed files with 595 additions and 37 deletions

View File

@ -0,0 +1,49 @@
package org.spiderflow.io;
public class Line {
private long from;
private String text;
private long to;
public Line(long from, String text, long to) {
this.from = from;
this.text = text;
this.to = to;
}
public long getFrom() {
return from;
}
public void setFrom(long from) {
this.from = from;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public long getTo() {
return to;
}
public void setTo(long to) {
this.to = to;
}
@Override
public String toString() {
return "Line{" +
"from=" + from +
", text='" + text + '\'' +
", to=" + to +
'}';
}
}

View File

@ -0,0 +1,146 @@
package org.spiderflow.io;
import java.io.Closeable;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class RandomAccessFileReader implements Closeable {
private RandomAccessFile raf;
/**
* 从index位置开始读取
*/
private long index;
/**
* 读取顺序默认倒叙
*/
private boolean reversed;
/**
* 缓冲区大小
*/
private int bufSize;
public RandomAccessFileReader(RandomAccessFile raf, long index, boolean reversed) throws IOException {
this(raf, index, 1024, reversed);
}
public RandomAccessFileReader(RandomAccessFile raf, long index, int bufSize, boolean reversed) throws IOException {
if (raf == null) {
throw new NullPointerException("file is null");
}
this.raf = raf;
this.reversed = reversed;
this.bufSize = bufSize;
this.index = index;
this.init();
}
private void init() throws IOException {
if (reversed) {
this.index = this.index == -1 ? this.raf.length() : Math.min(this.index, this.raf.length());
} else {
this.index = Math.min(Math.max(this.index, 0), this.raf.length());
}
if (this.index > 0) {
this.raf.seek(this.index);
}
}
/**
* 读取n行
*
* @param n 要读取的行数
* @param keywords 搜索的关键词
* @param matchcase 是否区分大小写
* @param regx 是否是正则搜索
* @return 返回Line对象包含行的起始位置与终止位置
*/
public List<Line> readLine(int n, String keywords, boolean matchcase, boolean regx) throws IOException {
List<Line> lines = new ArrayList<>(n);
long lastCRLFIndex = reversed ? this.index : (this.index > 0 ? this.index + 1 : -1);
boolean find = keywords == null || keywords.isEmpty();
Pattern pattern = regx && !find ? Pattern.compile(keywords) : null;
while (n > 0) {
byte[] buf = reversed ? new byte[(int) Math.min(this.bufSize, this.index)] : new byte[this.bufSize];
if (this.reversed) {
if (this.index == 0) {
break;
}
this.raf.seek(this.index -= buf.length);
}
int len = this.raf.read(buf, 0, buf.length);
if (len == -1) { //已读完
break;
}
for (int i = 0; i < len && n > 0; i++) {
int readIndex = reversed ? len - i - 1 : i;
if (isCRLF(buf[readIndex])) { //如果读取到\r或\n
if (Math.abs(this.index + readIndex - lastCRLFIndex) > 1) { //两行之间的间距,=1时则代表有\r\n,\n\r,\r\r,\n\n四种情况之一
long fromIndex = reversed ? this.index + readIndex : lastCRLFIndex; //计算起止位置
long endIndex = reversed ? lastCRLFIndex : this.index + readIndex; //计算终止位置
Line line = readLine(fromIndex + 1, endIndex); //取出文本
if (find || (find = (pattern == null ? find(line.getText(), keywords, matchcase) : find(line.getText(), pattern)))) { //定位查找使被查找的行始终在第一行
if (reversed) {
lines.add(0, line); //反向查找时插入到List头部
} else {
lines.add(line);
}
n--;
}
}
lastCRLFIndex = this.index + readIndex; //记录上次读取到的\r或\n位置
}
}
if (!reversed) {
this.index += buf.length;
}
}
if (reversed && n > 0 && lastCRLFIndex > 1 && (find || lines.size() > 0)) {
lines.add(0, readLine(0, lastCRLFIndex));
}
return lines;
}
private boolean find(String text, String keywords, boolean matchcase) {
return matchcase ? text.contains(keywords) : text.toLowerCase().contains(keywords.toLowerCase());
}
private boolean find(String text, Pattern pattern) {
return pattern.matcher(text).find();
}
/**
* 从指定位置读取一行
*
* @param fromIndex 开始位置
* @param endIndex 结束位置
* @return 返回Line对象
* @throws IOException
*/
private Line readLine(long fromIndex, long endIndex) throws IOException {
long index = this.raf.getFilePointer();
this.raf.seek(fromIndex);
byte[] buf = new byte[(int) (endIndex - fromIndex)];
this.raf.read(buf, 0, buf.length);
Line line = new Line(fromIndex, new String(buf), endIndex);
this.raf.seek(index);
return line;
}
private boolean isCRLF(byte b) {
return b == 13 || b == 10;
}
@Override
public void close() throws IOException {
if (this.raf != null) {
this.raf.close();
}
}
}

View File

@ -1,16 +1,8 @@
package org.spiderflow.controller;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.apache.commons.io.FileUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -22,6 +14,8 @@ import org.spiderflow.executor.FunctionExecutor;
import org.spiderflow.executor.FunctionExtension;
import org.spiderflow.executor.PluginConfig;
import org.spiderflow.executor.ShapeExecutor;
import org.spiderflow.io.Line;
import org.spiderflow.io.RandomAccessFileReader;
import org.spiderflow.model.Grammer;
import org.spiderflow.model.JsonBean;
import org.spiderflow.model.Plugin;
@ -33,9 +27,15 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* 爬虫Controller
@ -155,16 +155,14 @@ public class SpiderFlowController {
}
@RequestMapping("/log")
public String log(String id){
SpiderFlow flow = spiderFlowService.getById(id);
if(flow == null){
return "未找到此爬虫";
}
try {
return FileUtils.readFileToString(new File(spiderLogPath,id + ".log"), "UTF-8");
public JsonBean<List<Line>> log(String id, String keywords, Long index, Integer count, Boolean reversed,Boolean matchcase,Boolean regx){
try (RandomAccessFileReader reader = new RandomAccessFileReader(new RandomAccessFile(new File(spiderLogPath,id + ".log"),"r"), index == null ? -1 : index, reversed == null || reversed)){
return new JsonBean<>(reader.readLine(count == null ? 10 : count,keywords,matchcase != null && matchcase,regx != null && regx));
} catch(FileNotFoundException e){
return new JsonBean<>(0,"日志文件不存在");
} catch (IOException e) {
logger.error("读取日志文件出错",e);
return "读取日志文件出错" + e.getMessage();
return new JsonBean<>(-1,"读取日志文件出错");
}
}

View File

@ -0,0 +1,166 @@
function LogViewer(options){
options = options || {};
this.element = options.element;
this.maxLines = options.maxLines || 10;
this.onSearchFinish = options.onSearchFinish || function(){};
this.bufferSize = this.maxLines * 10;
this.logId = options.logId;
this.url = options.url;
this.buffer = [];
this.displayIndex = -1;
this.index = -1;
this.loading = false;
this.reversed = true;
this.matchcase = true;
this.initEvent();
this.init(options.onLoad);
}
LogViewer.prototype.init = function(callback){
var _this = this;
_this.index = -1;
this.autoLoad(callback);
}
LogViewer.prototype.autoLoad = function(callback){
var _this = this;
this.loadLines(this.maxLines,function(hasData){
if(_this.reversed){
_this.displayIndex = _this.buffer.length - _this.maxLines;
}else{
_this.displayIndex = 0;
}
_this.render(_this.buffer.slice(_this.displayIndex,_this.displayIndex + _this.maxLines));
callback&&callback(hasData);
},false);
}
LogViewer.prototype.render = function(lines){
if(lines.length == 0){
return;
}
this.firstFrom = lines[0].from;
this.firstTo = lines[0].to;
this.lastFrom = lines[lines.length - 1].from;
this.lastTo = lines[lines.length - 1].to;
var html = [];
if(this.reversed){
lines = lines.reverse();
}
var find = this.keywords === undefined || this.keywords === '';
var regx = new RegExp('(' + this.keywords + ')',this.matchcase ? "ig" : "g");
for (var i = 0; i < lines.length; i++) {
var text = lines[i].text;
if(find == false && (find = text.match(regx))){
text = text.replace(regx,'b4430885ba83495_$1_88d1220d37eac831d');
}
//转义html
text = text.replace(/</g,'&lt;');
//搜索关键词高亮
text = text.replace(/b4430885ba83495_(.*?)_88d1220d37eac831d/g,'<em class="search-finded">$1</em>');
html.push('<div class="log-row">' + text + '</div>');
}
if(this.reversed){
html = html.reverse();
}
this.element.html(html.join(''));
}
LogViewer.prototype.search = function(reversed){
if(reversed === undefined){
reversed = this.reversed;
}
this.index = reversed ? this.lastFrom : this.firstTo;
var _this = this;
this.autoLoad(function(hasData){
_this.onSearchFinish(hasData);
});
}
LogViewer.prototype.initEvent = function(){
var _this = this;
function eventFunc(e){
e.stopPropagation();
_this.scroll((e.wheelDelta||e.detail) > 0,3);
return false;
}
document.addEventListener('DOMMouseScroll',eventFunc,false);
window.onmousewheel = document.onmousewheel = eventFunc;
document.addEventListener('keydown', function (e) {
e = e || event;
var currKey = e.keyCode || e.which || e.charCode;
if (currKey === 38 || currKey === 40) {
if(_this.keywords){
_this.search(currKey === 38);
}else{
_this.scroll(currKey === 38, 1);
}
}
if (currKey === 33 || currKey === 34) {
_this.scroll(currKey === 33, _this.maxLines);
}
if (currKey === 36 || currKey ===35){
_this.reversed = currKey === 35;
_this.init();
}
});
}
LogViewer.prototype.setOptions = function(key,value){
var _this = this;
_this[key] = value;
}
LogViewer.prototype.scroll = function(reversed,count){
var _this = this;
_this.reversed = reversed;
var ignore = false;
if(reversed){
if(this.displayIndex == 0){
this.index = this.buffer[0].from;
this.loadLines(this.bufferSize,function(hasData){
if(hasData){
_this.displayIndex = Math.max(_this.buffer.length - _this.maxLines,0);
}
},false);
}else{
_this.displayIndex-=count;
}
}else{
if(this.displayIndex + this.maxLines >= this.buffer.length){
this.index = this.buffer[this.buffer.length - 1].to;
this.loadLines(this.bufferSize,function(hasData){
if(hasData){
_this.displayIndex = 0;
}
},false);
}else{
_this.displayIndex+=count;
}
}
this.render(this.buffer.slice(this.displayIndex,this.displayIndex + this.maxLines));
}
LogViewer.prototype.loadLines = function(count,callback,async){
if(this.loading){
return;
}
this.loading = true;
var _this = this;
$.ajax({
url : this.url,
async : async,
type : 'post',
data : {
reversed : this.reversed,
count : this.bufferSize,
id : this.logId,
index : _this.index,
keywords : this.keywords,
matchcase : this.matchcase,
regx : this.regx
},
dataType : 'json',
success : function(json){
var hasData = json&&json.data&&json.data.length > 0;
if(hasData){
_this.buffer = json.data;
}
callback && callback(hasData);
_this.loading = false;
}
})
}

View File

@ -5,28 +5,227 @@
<title>SpiderFlow</title>
<script type="text/javascript" src="js/layui/layui.all.js" ></script>
<script type="text/javascript" src="js/common.js" ></script>
<script>$ = layui.$;</script>
<script type="text/javascript" src="js/log-viewer.js" ></script>
<style type="text/css">
html,body,.log-container{
*{
margin:0;
padding:0;
}
html,body{
width : 100%;
height : 100%;
font-weight: bold;
font-family: Consolas;
overflow: hidden;
}
html,body{
overflow: hidden;
}
.log-container{
width : 100%;
position: absolute;
top : 0px;
bottom : 40px;
overflow: auto;
background-color: #000;
}
.toolbox-container{
width : 100%;
position: absolute;
height : 24px;
padding:8px 0;
bottom : 0px;
background: #3c3f41;
}
.toolbox-container .input-text{
outline: 0;
height: 24px;
line-height: 24px;
margin-left: 7px;
background: #45494a;
border: 1px solid #646464;
width: 300px;
color: #ddd;
padding-left: 5px;
font-size: 14px;
float : left;
margin-right: 20px;
}
.toolbox-container .input-text.search-finish{
background: #743a3a;
}
.toolbox-container .input-checkbox{
visibility: hidden;
}
.toolbox-container .input-checkbox +label{
color : #c9c9c9;
float : left;
font-size:12px;
height:24px;
line-height: 24px;
margin-left: 25px;
margin-right:5px;
user-select: none;
}
.toolbox-container .input-checkbox +label::before{
display: inline-block;
background: #43494a;
border:1px solid #6b6b6b;
content : '';
width : 16px;
height : 16px;
line-height: 16px;
position : absolute;
top : 12px;
margin-left:-22px;
}
.toolbox-container .input-checkbox:checked +label::before{
display : inline-block;
content : "\2714";
text-align: center;
font-size:12px;
color:#fff;
}
.toolbox-container .btn{
display: inline-block;
width : 24px;
height : 24px;
border-radius: 2px;
background-repeat: no-repeat;
background-position: center center;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAoklEQVQ4T63SwQnCQBCF4fdIN9qEZxEMgqnAGqzDDgSPCZgYgwcvVmGKEeLIGFZwHEWW7HX34x+WISIPIx2GhVV1Ggm7HYgb78kqTaetneyjqAjscgHG+pjAFZJkFr9Bi0LFwy9okYBZX5TcKz+hhxbzWaF3+0Oz9DB/oTCqh1nWxwsEE32k44WS/UWLWdbNVqEA62/IlDcgz8MuwD9rGF18AERoXOuD03ayAAAAAElFTkSuQmCC");
float : left;
}
.btn.btn-prev{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAoklEQVQ4T63SwQnCQBCF4fdIN9qEZxEMgqnAGqzDDgSPCZgYgwcvVmGKEeLIGFZwHEWW7HX34x+WISIPIx2GhVV1Ggm7HYgb78kqTaetneyjqAjscgHG+pjAFZJkFr9Bi0LFwy9okYBZX5TcKz+hhxbzWaF3+0Oz9DB/oTCqh1nWxwsEE32k44WS/UWLWdbNVqEA62/IlDcgz8MuwD9rGF18AERoXOuD03ayAAAAAElFTkSuQmCC");
}
.btn.btn-next{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAqElEQVQ4T73SzQ2CQBAF4PdCN9iEZ2MiMZEKrME67MDEIySASDx4sQotxgTHjLBIcOCwB/e6+2Xf/BCeh54O/jAvqwMEcwF269UynUqQnaoNIXuQV+bl+aZQgYDxGG5Rou8I3FkUlxCsEwFmY3iIIEH8qXEKWyiKFo+uORZuYkkXT39S1Mb9tmOI3Y3W1Ec/0IptIRM6LKyPIJ58BVsXrz8q/wX4+8q9AR2wXOs7tERxAAAAAElFTkSuQmCC");
}
.btn.btn-page-prev{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABIElEQVQ4T92SP0sDQRDF31vSieA3sLMQrLQVvE7kktsVIljYWAgGsbDQwtZGGwsRNKUgQoTs3h9t4xew1MLSL6EW3kjucppcEu3danfe/N4Ms0OMOM65OajKogAfU5MT157nvZfTWA60w3CZoi5ATHc1QjqQdEdr/dyfOwC2w2STkEsAlZLhqwIbQeDfFfFv0EXJoYgcZYLgBcTM0D2VXWNqZ3knAKyLzkE2cje5QcomFDrZM4UHJVsA1zOAPNU1f482jFoA670WDkxQPbE2XuoHjak+2DDeB3DcM2/SRvEVBBsCrq0G/m3WwQiwG2+HSZ2QlgK2h6b6Gzh2qoUwruJ/Ap27nxV+PuX/yAVj/Mc/d/VnQMk8qd60XhnY0UL/AhYefjeyfy3+AAAAAElFTkSuQmCC");
}
.btn.btn-page-next{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABGklEQVQ4T+VSsUoDQRSctwGxzCcICnZWfoBiIcjldlc/QQhWgihYGlsVC0VQmxSWajbvPIhoEX/AQgQ7wX+w9Ngn2VM4k80X+Kp9szMLOzOEMcN8PyPiJ4xZeYtRKAYy9+Y8ipdw52ne2uR5mBcVOne3AIV+KcSitY2nfyl0nG8DcqRAWuskG5gwzhzmPPUQBuSaOt3smIi2goHe769Z3YoJbx23lFJ7pbtyGeLoZPkhiewECHDkcVKNQxQ2CbBBQ3Rq02Swl+M4bwJyUW70Dsj06Bm7VjcOAqMa7E03X67Bn4No6m/g9AXy6zZNr37xkeYw92Y9FWcQLP3850MgzVWtH6uPRSvXbvcn6/XPDShVIykejDGvw5X7BuachnZstBghAAAAAElFTkSuQmCC");
}
.btn.btn-page-home{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAA/klEQVQ4T9WRsUoDURBF7107O/0Av8DCP1CwsNnNMinW3kJELAzYpom9WKXTSmxEXzazlmJrY+cX+A2iguSN7BPDZvMghZVTDMPMPXNhhgDgxvoIw1ZdLwxCpZN1+CdwoUtEEBzboarLE2PPzFYkz05imjnQufsNLPlzGDYDQF58fb4fF0Xx1lwwA96OdDdJOITZalNksCf6yZGIPP/2p2Cp1cB76/8MeAjYsFV/ANyXPL0KijrdjcZnJHsAXuGxJ5I9uFKtnkme0TndRoJLAGsGFt08vQlgWeqBJ3YsYb+bpi/hRQ0wLK+qdXo7NeP1FIxdrQ22NdF3xBz/MfgNtRhyDx//lbEAAAAASUVORK5CYII=");
}
.btn.btn-page-end{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAA/0lEQVQ4T91SsU4CQRSct8QEjd/AF9DR0BJL73EsxoZYSUdl7LWhNcHCGGNBzxVsjruGggT/wMIv4AOws/PGcKgceqeJpVtt3pvZ93ZmBAXHhRFXLeur5EFyiyvgfyOOw/hYhB0auWh73lPeH8dxXJWEfUNMfV/vUnFSIhgAWCDBqbU6y4rjXHQAgyGACslBu9U8/1TVhfEJwHsAu4D0AN6ubdjcjZG+r95lWs165Jyr0ZRuBFLf8k5kmSTsHbV09FH/5mMQBPs75b1rkN31QDzg1ZxZe/iYfeynAFyJyHNJOFDVl6/pKSQWRXFrVTeJJiD0N/D76nPb1EY68S/EN0z7aw9vab4+AAAAAElFTkSuQmCC");
}
.toolbox-container .btn:hover{
background-color: #4c5052;
}
::-webkit-input-placeholder {
color: #eee;
}
.log-row{
font-family: Consolas,serif;
font-size: 12px;
line-height: 14px;
height : 14px;
white-space: pre;
color: #e5e5e5;
background: #000;
}
.log-row::selection{
background: rgba(255,255,255,.998);
color : #000;
}
.log-row em.search-finded{
font-style: inherit;
background :#ff0 !important;
color : #000;
}
::-webkit-scrollbar{
width : 8px;
height : 8px;
background: transparent;
}
::-webkit-scrollbar-track{
border-radius: 2px;
}
::-webkit-scrollbar-thumb{
border-radius: 2px;
background: #999;
}
</style>
</head>
<div class="log-container"></div>
<div class="toolbox-container">
<input type="text" id="keywords" placeholder="请输入关键词搜索定位日志" class="input-text"/>
<input type="checkbox" id="reversed" checked class="input-checkbox"/>
<label for="reversed">反向搜索</label>
<input type="checkbox" id="matchcase" checked class="input-checkbox"/>
<label for="matchcase">区分大小写</label>
<input type="checkbox" id="regx" class="input-checkbox"/>
<label for="regx">正则搜索</label>
<span class="btn btn-prev" title="上一个/上一行(↑)"></span>
<span class="btn btn-next" title="下一个/下一行(↓)"></span>
<span class="btn btn-page-prev" title="上一页(Page Up)"></span>
<span class="btn btn-page-next" title="下一页(Page Down)"></span>
<span class="btn btn-page-home" title="第一页/首页(Home)"></span>
<span class="btn btn-page-end" title="最后一页/尾页(End)"></span>
</div>
<script type="text/javascript">
var id = getQueryString('id');
layui.$.ajax({
$(function(){
var viewer = new LogViewer({
url: 'spider/log',
data : {
id : id
maxLines : parseInt(($('.log-container').height() - 8) / 14),
logId : getQueryString('id'),
element : $('.log-container'),
onSearchFinish : function(hasData){
if(hasData){
$('.input-text').removeClass('search-finish');
}else{
$('.input-text').addClass('search-finish').focus();
}
},
dataType : 'text',
success : function(content){
layui.$('.log-container').html(content.replace(/\n/g,'<br>').replace(/ /g,'&nbsp;').replace(/\t/g,'&nbsp;&nbsp;&nbsp;&nbsp;'));
onLoad : function(hasData){
if(!hasData){
layui.layer.alert('日志文件不存在');
}
}
})
var setOptions = function(){
viewer.setOptions('keywords',$('.toolbox-container .input-text').val());
viewer.setOptions('matchcase',$('#matchcase').is(':checked'));
viewer.setOptions('regx',$('#regx').is(':checked'));
viewer.setOptions('reversed',$('#reversed').is(':checked'));
}
$('.toolbox-container').on('keydown','.input-text',function(e){
setOptions();
if(e.keyCode === 13){
viewer.search();
}
if(this.value === ''){
$(this).removeClass('search-finish');
}
}).on('change','.input-checkbox',function(){
setOptions();
}).on('click','.btn-prev',function(){
if(viewer.keywords){
viewer.search(true);
}else{
viewer.scroll(true,1);
}
}).on('click','.btn-next',function(){
if(viewer.keywords){
viewer.search(false);
}else{
viewer.scroll(false,1);
}
}).on('click','.btn-page-prev',function(){
viewer.scroll(true,viewer.maxLines);
}).on('click','.btn-page-next',function(){
viewer.scroll(false,viewer.maxLines);
}).on('click','.btn-page-home',function(){
viewer.setOptions('reversed',false);
viewer.init();
}).on('click','.btn-page-end',function(){
viewer.setOptions('reversed',true);
viewer.init();
});
});
</script>
</html>