章节导航
从分析需求入手,查找资料,制定方案,通过ctrlCV大法实现目的。
需求分析
平时打发时间看些小说,有时从网上找一些txt的资源,在安卓手机上有非常好用而且顺眼的阅读APP,但是小小屏幕看久了总是会累的。用电脑看,视野开阔,阅读效率也高,然而目前PC阅读常见的方案无论是网站阅读(页面配色不够舒适,广告多)还是EXE阅读(如TXT小说阅读器,虽然用着不错,仍嫌配色不够护眼)总是差强人意,所以我就需要有一个能满足护眼需求的阅读工具。
考虑自己半吊子的QT能力和前后端水平,罗列了几种可用方案,并最终选择了纯html+css+js的简单实现。
- 纯QT实现 ,考虑界面如何自适应会比较麻烦,虽然是在看小说,但是既然是在电脑上,就要考虑应用遮挡问题,难免需要调整窗口大小。另外学艺不精,配置各种图标和做中文处理的时候难免遇到各种问题;由于UI美工能力不足,需要不停调试效果,用QT一次次编译生效实在有点恶心,想做点能在使用时对阅读器效果灵活配置的功能会不断增加项目规模,然而我只是想爱眼阅读而已。
- 本地WEB 做界面,还是web前端三剑客实现起来容易。无论是划分功能块、调整样式,还是添加操作上的交互,都很容易。基本上浏览器也都支持打开本地文件。
- QT+本地WEB 其实是有些配置能存下来会比较好的,比如历史文件、阅读到的位置、字号颜色等配置,要么前台cache,或者用后台,后台用jar或者c都行,c的话新建个qt项目就是了,那么把web直接嵌到QT界面上并实现通信就是个容易想到的方案。不过也有些“成本”问题,后台读写配置要么用配置文件读写,要么连个redis或mysql,终归只是想实现些锦上添花的功能,却要多实现不少东西。而且虽然没有尝试,但很可能打开文件时要从浏览器支持的FileReader换成从C读取文件并发内容给WEB了。
- 本地WEB页面丢到NGINX里 想用cache存一些配置数据,用本地web文件是不行的,丢到ngnix里面去,通过http访问,操作起来还挺简单。
那么我对功能点的具体需求是什么呢?
- 没有乱七八糟的多余元素 自己从头做起来的东西,有多余的东西倒还比较难
- 加载文档最基本都需求。IE以外的浏览器基本都能很方便的支持
- 修改字色、背景色、字号等做这个东西都核心目的。灵活点可以整个调色盘,简单点就挑些配色做下拉框
- 定位/章节导航切割文档,类似翻页。正则匹配,用通用点的规则,结果可能就没那么精准,按需取舍
- 保存阅读配置至少记录下读到的章节
- 翻页/翻章节 ctrl + ←→方向键
HTML代码
<!DOCTYPE html>
<html>
<head>
<title>本地TXT阅读</title>
<meta name="name" content="content" charset="utf-8">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/all.css">
<script src='https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js' type='text/javascript'></script>
<script src='./settings.js' type='text/javascript'></script>
</head>
<body>
<input type="file" id="file" />
<input type="button" onclick="readText()" value="打开">
<input type='number' id="fontsizt" min='16' max='32' value='22'/>
<input type="button" onclick="setSize()" value="设置字号">
<input type='number' id="paragraght" min='1' max='' value='1'/>
<input type="button" onclick="setParagraph()" value="设置章节">
<input type="button" id="prev" onclick="Number.parseInt(document.getElementById('paragraght').value)>1&&document.getElementById('paragraght').value--;setParagraph()" value="上一章">
<input type="button" id="next" onclick="Number.parseInt(document.getElementById('paragraght').value)<Number.parseInt(document.getElementById('paragraght').max)&&document.getElementById('paragraght').value++;setParagraph()" value="下一章">
<div id="top" style="position:absolute;top:2px;right:10px">
颜色: <select id="fcolor" onchange="setColor(this)"><option>选择</option>
<option value="blue" style="color:blue">蓝色</option>
<option value="yellow" style="color:yellow;background-color:#73B6EF">黄色</option>
<option value="navy" style="color:navy">海军蓝</option>
<option value="royalblue" style="color:royalblue">皇家蓝</option>
<option value="green" style="color:green">深绿色</option>
<option value="purple" style="color:purple">紫色</option>
<option value="olive" style="color:olive">橄榄色</option>
<option value="DarkCyan" style="color:DarkCyan">深青色</option>
<option value="lime" style="color:lime">鲜绿色</option>
<option value="gray" style="color:#d8d6d0">灰色</option>
<option value="#FFF" style="color:white;background-color:#73B6EF">白色</option></select>
背景色: <select id="bkcolor" onchange="setBack(this)"><option>选择</option>
<option value="wheat" style="background-color:wheat">小麦色</option>
<option value="Gainsboro" style="background-color:Gainsboro">淡灰色</option>
<option value="lightgreen" style="background-color:lightgreen">淡绿色</option>
<option value="khaki" style="background-color:khaki">黄褐色</option>
<option value="yellowgreen" style="background-color:yellowgreen">黄绿色</option>
<option value="lightblue" style="background-color:lightblue">淡蓝色</option>
<option value="violet" style="background-color:violet">紫罗兰</option>
<option value="#000" style="background-color:#000;color:#eee">黑色</option>
<option value="AntiqueWhite" style="background-color:AntiqueWhite">古典白</option>
<option value="#F2FD84" style="background-color:#F2FD84">#F2FD84</option></select>
<table><tbody><tr><td width="120"><span></span></td></tr></tbody></table>
</div>
<input type="checkbox" id="check">
<label for="check">
<i class="fas fa-bars" id="btn"></i>
<i class="fas fa-times" id="cancel"></i>
</label>
<div class="sidebar">
<header>目录</header>
<ul id="leftNav">
<!-- <li><a href="#"><i class="fas fa-qrcode"></i>Dashboard</a></li> -->
<!-- <li><a href="#"><i class="fas fa-link"></i>Shortcuts</a></li> -->
<li><a href="#"><i class="fas fa-stream"></i>Overview</a></li>
<!-- <li><a href="#"><i class="fas fa-calendar-week"></i>Events</a></li> -->
<!-- <li><a href="#"><i class="far fa-question-circle"></i>About</a></li> -->
<!-- <li><a href="#"><i class="fas fa-sliders-h"></i>Services</a></li> -->
<!-- <li><a href="#"><i class="far fa-envelope"></i>Contact</a></li> -->
</ul>
</div>
<div class="showtext">
<pre id="tt" style="word-wrap: break-word; white-space: pre-wrap;font-weight: bolder; font-family: 微软雅黑;">
</pre>
</div>
</body>
</html>
<style>
</style>
<script charset="utf-8">
window.onload=function () {
styleObj=document.styleSheets[2]
if(typeof(FileReader)=="undefined")
{
alert("你的浏览器不支持文件读取");
document.write("");
}else
{
console.log("你的浏览器支持文件读取");
}
/*var jQueryElement=document.createElement('script');
jQueryElement.type = 'text/javascript';
jQueryElement.onload = function () {
console.log('jQuery loaded');
setSize();
};
jQueryElement.src = 'https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js';
document.getElementsByTagName('head')[0].appendChild(jQueryElement);*/
s1=document.getElementById("fcolor");
s2=document.getElementById("bkcolor");
s1.selectedIndex=10;
s2.selectedIndex=8;
ev = document.createEvent("HTMLEvents");
ev.initEvent("change", false, true);
s1.dispatchEvent(ev) ;//fireEvent不支持
s2.dispatchEvent(ev) ;
setSize();
$(document).keydown(function(event){
if(event.ctrlKey && event.keyCode == 37){
$('#prev').click();
}else if(event.ctrlKey && event.keyCode == 39){
$('#next').click();
}
});
}
//1 浏览器打开本地文档 https://zhidao.baidu.com/question/1541329842508075787.html
//2 修改颜色select+option选择颜色(含cookie设置,注意cookie无法在本地web页面使用) https://www.shouce.ren/api/html/html4/tools-color_selector.html
//3 章节正则匹配识别 https://blog.csdn.net/weixin_29143935/article/details/114791177 https://blog.csdn.net/weixin_42448623/article/details/102785880
//4 JSON有效性判断 https://www.cnblogs.com/lanleiming/p/7096973.html
//5 左侧章节导航栏 https://blog.csdn.net/qq_25503949/article/details/106244548
//6 滚动条样式修改(webkit) https://jingyan.baidu.com/article/e75057f2017b2debc81a8968.html
//7 滚动条设置scrollTop时平滑过度 https://blog.51cto.com/u_15693675/5407922
//8 键盘监听 https://www.cnblogs.com/pangpanghuan/p/6423204.html
//9 setcookie要给个时间,以免关闭浏览器(会话结束)时被清除https://www.csdn.net/tags/Ntjacg2sMTI4MjktYmxvZwO0O0OO0O0O.html
</script>
CSS代码
style.css
* {
margin: 0;
padding: 0;
list-style: none;
text-decoration: none;
font-family: "Roboto", sans-serif;
}
html,body,#page{
height: 100%;
width: 100%;
}
pre{
height: 100%;
width: 100%;
}
body{
overflow:hidden;
}
#tt{
overflow-y:auto;
}
::-webkit-Scrollbar{
width:10px;
height:10px;
}
::-webkit-Scrollbar-thumb{
border-radius:5px;
-webkit-box-shadow:inset 0 0 5px rgba(0,0,0,0.2);
background:rgba(255,255,255,0.2);
}
::-webkit-Scrollbar-track{
-webkit-box-shadow:inset 0 0 5px rgba(0,0,0,0.2);
border-radius:0;
background:rgba(255,255,255,0.1);
}
.sidebar {
position: fixed;
left: -250px;
width: 250px;
height: 100vh;
background-color: #042331;
transition: all 0.5s ease;
//ease 开始和结束慢,中间快
}
.sidebar header {
color: white;
text-align: center;
line-height: 45px;
font-size: 22px;
background: #0d547633;
user-select: none;
//用户不能选取
}
.sidebar ul a {
display: block;
padding-left: 20px;
line-height: 34px;
border-top: 1px solid rgba(255, 255, 255, .1);
border-bottom: 1px solid #a5a5a5;
color: white;
transition: 0.3s linear;
}
.sidebar ul{
/*leftNav*/
overflow-y:auto;
height:90%;
scroll-behavior: smooth;
}
.sidebar ul i {
padding-right: 16px;
}
.sidebar ul a:hover {
padding-left: 10px;
}
#check {
display: none;
}
label #btn,
label #cancel {
position: absolute;
cursor: pointer;
background-color: #3aa327;
border-radius: 3px;
}
label #btn {
left: 0px;
top: 25px;
color: black;
font-size: 18px;
padding: 3px 9px;
transition: 0.3s;
z-index:1;
}
label #cancel {
z-index: 999;
left: -197px;
top: 29px;
font-size: 30px;
color: #0a5275;
padding: 4px 9px;
transition: 0.3s;
}
#check:checked~.sidebar {
left: 0px;
}
.showtext{
position: absolute;
height: 94%;
width: 98%;
padding: 22px;
left:0px;
transition: all 0.5s ease;
}
#check:checked~.showtext {
left: 250px;
width: calc(98% - 250px);
}
#check:checked~label #btn {
left: 250px;
opacity: 0;
}
#check:checked~label #cancel {
left: 208px;
}
section {
background: url(./bg.png) no-repeat;
background-position: center;
//定位背景图片在中间
height: 100vh;
background-size: cover;
transition: all 0.5s;
}
#check:checked~section {
margin-left: 250px;
}
【~】号说明:参考的文章里解释的不太好,可以看这个
General_sibling_combinator说明
A~B:A和B是两个筛选器。与A同层级,同父级,且位置在A后的,所有用B筛选出的元素的样式。
JS代码
setting.js
function readText() {
var file=document.getElementById("file").files[0];
console.log(document.getElementById("file").files);
curFilename=file.name;//全局
var reader=new FileReader();
reader.readAsText(file,"GB2312");
reader.onload=function(data)
{
var tt=document.getElementById("tt")
//tt.innerHTML=this.result;
console.log(this.result.length);
var titleRule=/\s*(第)([一二三四五六七八九十零百千万]{1,9})[章](\s*)(\S*)(\n|\r|\r\n)/g; // \s*(第)(.{1,9})[章节卷集部篇回](\s*)(\S*)(\n|\r|\r\n)
var titleRuleS=/(第)(.{1,9})[章](\s*)(\S*)/;
//进行章节标题匹配查找
titles=this.result.match(titleRule);//全局
//todo:处理章节标题
fileTitle=[]//全局
var titleUL="";
titles.forEach(function(e){
var titlenow=e.match(/(第)(.{1,9})[章](\s*)(\S*)/)[0];
//console.log(titlenow+"at"+dtxt.indexOf(titlenow))
fileTitle.push(titlenow);
titleUL+='<li><a href="#" οnclick="loadData(\''+titlenow+'\')"><i class="fas fa-stream"></i>'+titlenow+'</a></li> \
';
})
document.getElementById('leftNav').innerHTML=titleUL;
//切割文档
fileData=[]//全局
for(var i=0;i<fileTitle.length-1;i++){
fileData.push(this.result.substring(this.result.indexOf(fileTitle[i]),this.result.indexOf(fileTitle[i+1])))
}
fileData.push(this.result.substring(this.result.indexOf(fileTitle[fileTitle.length-1])))
//加载文档
currentPage=0;//全局
var cache = getCookie("localReadCache");
if(isJSON(cache)){
var cacheJSON=JSON.parse(cache);
if(cacheJSON.hasOwnProperty(curFilename)){
currentPage=cacheJSON[curFilename];
document.getElementById('paragraght').value=Number.parseInt(currentPage+1);
$('#leftNav').scrollTop($('.sidebar ul a')[currentPage-1].offsetTop)
}
}
tt.innerHTML=fileData[currentPage];
maxPage=fileTitle.length-1;//全局
var para=document.getElementById("paragraght");
para.max=maxPage+1;
}
}
function setSize(){
var fontsize=document.getElementById("fontsizt");
if (Number.parseInt(fontsize.value)>=Number.parseInt(fontsize.min)){
var tt = document.getElementById('tt');
tt.style.fontSize=fontsize.value+"px";
}
}
function setParagraph(){
var para=document.getElementById("paragraght");
if (Number.parseInt(para.value)>=Number.parseInt(para.min)&&Number.parseInt(para.value)<=Number.parseInt(para.max)){
var index=Number.parseInt(para.value)-1;
var data=fileData[index];
var tt=document.getElementById("tt");
tt.innerHTML=fileData[index];
currentPage=index;
$('#leftNav').scrollTop($('.sidebar ul a')[currentPage-1].offsetTop)
var cache = getCookie("localReadCache");
if(isJSON(cache)){
var cacheJSON=JSON.parse(cache);
cacheJSON[curFilename]=currentPage;
setCookie("localReadCache",JSON.stringify(cacheJSON),86400*60);
}else{
var newCache={
};
newCache[curFilename]=currentPage;
setCookie("localReadCache",JSON.stringify(newCache),86400*60);
}
}
}
function loadData(title){
var index=fileTitle.indexOf(title);
if(index>-1){
var data=fileData[index];
var tt=document.getElementById("tt");
tt.innerHTML=fileData[index];
currentPage=index;
document.getElementById('paragraght').value=Number.parseInt(currentPage+1);
var cache = getCookie("localReadCache");
if(isJSON(cache)){
var cacheJSON=JSON.parse(cache);
cacheJSON[curFilename]=currentPage;
setCookie("localReadCache",JSON.stringify(cacheJSON),86400*60);
}else{
var newCache={
};
newCache[curFilename]=currentPage;
setCookie("localReadCache",JSON.stringify(newCache),86400*60);
}
}
}
function isJSON(str) {
if (typeof str == 'string') {
try {
var obj=JSON.parse(str);
if(typeof obj == 'object' && obj ){
return true;
}else{
return false;
}
} catch(e) {
console.log('error:'+str+'!!!'+e);
return false;
}
}
console.log('It is not a string!')
}
//liuming [email protected] QQ395310500
//flow
// var styleObj=document.styleSheets[2]
//设置cookie
function setCookie(name,value,expires){
//var curCookie=name+"="+escape(value)+((expires)?";expires="+expires.toGMTString():"");
//document.cookie=curCookie;
var exp = new Date();
exp.setTime(exp.getTime() + (expires?expires:0)*1000);
var curCookie=name+"="+escape(value)+((expires)?";expires="+exp.toGMTString():"");
document.cookie=curCookie;
}
//取出cookie
function getCookie(name)
{
var aCookie = document.cookie.split("; ");
for (var i=0; i < aCookie.length; i++)
{
var aCrumb = aCookie[i].split("=");
if (name == aCrumb[0])
return unescape(aCrumb[1]);
}
return null;
}
//删除cookie
function deleteCookie(name) {
if (getCookie(name)) {
document.cookie = name + "=; expires=Thu, 01-Jan-70 00:00:01 GMT";
}
}
//flow
function setFont(select){
var fontSize=select.value+"pt";
if (document.all){
styleObj.addRule("body","font-size:"+fontSize);
}else{
styleObj.insertRule("body{font-size:"+fontSize+"}",document.styleSheets[0].cssRules.length);
}
}
function setColor(select){
var color=select.value;
if (document.all){
styleObj.addRule("body","color:"+color);
}else{
styleObj.insertRule("body{color:"+color+"}",document.styleSheets[2].cssRules.length);
}
}
function setBack(select){
var bgcolor=select.value;
if (document.all){
styleObj.addRule("body","background-color:"+bgcolor);
}else{
styleObj.insertRule("body{background-color:"+bgcolor+"}",document.styleSheets[2].cssRules.length);\
}
}
总结
纯纯是ctrlCV,献丑了。