From da7a243387e893a20645e360f19ef8597fd634c8 Mon Sep 17 00:00:00 2001 From: tumuyan Date: Tue, 3 Nov 2020 01:40:45 +0800 Subject: [PATCH] =?UTF-8?q?1.=E5=9C=A8=E4=B9=A6=E6=9E=B6=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E4=B9=A6=E7=B1=8D=E7=9A=84=E7=95=8C=E9=9D=A2=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=A4=8D=E5=88=B6=E5=B0=8F=E8=AF=B4=E7=BD=91=E5=9D=80?= =?UTF-8?q?=E7=9A=84=E8=8F=9C=E5=8D=95=E3=80=82=202.=E5=9C=A8=E4=B9=A6?= =?UTF-8?q?=E6=BA=90=E5=88=97=E8=A1=A8=E7=95=8C=E9=9D=A2=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=A3=80=E6=9F=A5=E4=B9=A6=E6=BA=90=E6=98=AF=E5=90=A6?= =?UTF-8?q?=E5=8C=85=E5=90=AB=E5=8F=91=E7=8E=B0=E8=A7=84=E5=88=99=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E5=A2=9E=E5=8A=A0/=E5=88=A0=E9=99=A4=E2=80=9C?= =?UTF-8?q?=E5=8F=91=E7=8E=B0=E2=80=9D=E6=A0=87=E7=AD=BE=E7=9A=84=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E3=80=82=203.=E5=9C=A8=E9=98=85=E8=AF=BB=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=EF=BC=8C=E5=A2=9E=E5=8A=A0=E9=95=BF=E6=8C=89=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E2=80=9C=E5=B9=BF=E5=91=8A=E2=80=9D=EF=BC=8C=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E5=BF=AB=E9=80=9F=E6=8A=8A=E6=AD=A3=E6=96=87=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=88=B0=E5=91=BD=E5=90=8D=E4=B8=BA=E2=80=9C=E5=B9=BF?= =?UTF-8?q?=E5=91=8A=E8=AF=9D=E6=9C=AF-xxx=E7=BD=91=E5=9D=80=E2=80=9D?= =?UTF-8?q?=E7=9A=84=E8=A7=84=E5=88=99=E4=B8=AD=E5=8E=BB=E3=80=82=E9=92=88?= =?UTF-8?q?=E5=AF=B9=E6=AD=A4=E8=A7=84=E5=88=99=EF=BC=8C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E4=BA=86=E4=B8=93=E9=97=A8=E7=9A=84=E5=A2=9E=E5=BC=BA=E7=AE=97?= =?UTF-8?q?=E6=B3=95=E3=80=82=E5=8F=AA=E9=9C=80=E8=A6=81=E5=A4=9A=E6=AC=A1?= =?UTF-8?q?=E6=A0=87=E8=AE=B0=E4=B8=8D=E6=83=B3=E7=9C=8B=E7=9A=84=E6=96=87?= =?UTF-8?q?=E5=AD=97=EF=BC=8C=E5=B0=B1=E5=8F=AF=E4=BB=A5=E8=8E=B7=E5=BE=97?= =?UTF-8?q?=E8=BE=83=E5=A5=BD=E7=9A=84=E5=B9=BF=E5=91=8A=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E6=95=88=E6=9E=9C=E3=80=82=E7=94=A8=E6=88=B7=E4=B8=8D=E9=9C=80?= =?UTF-8?q?=E8=A6=81=E7=86=9F=E6=82=89=E6=AD=A3=E5=88=99=E8=AF=AD=E6=B3=95?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E9=9C=80=E8=A6=81=E7=BB=B4=E6=8A=A4=E5=A4=8D?= =?UTF-8?q?=E6=9D=82=E7=9A=84=E8=A7=84=E5=88=99=E3=80=82=E5=90=8C=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E7=BD=91=E7=AB=99=E7=9A=84=E5=B9=BF=E5=91=8A=E8=AF=9D?= =?UTF-8?q?=E6=9C=AF=E8=87=AA=E5=8A=A8=E4=BF=9D=E5=AD=98=E5=9C=A8=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E8=A7=84=E5=88=99=E5=86=85=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E4=BA=86=E2=80=9C=E6=9B=BF=E6=8D=A2=E5=87=80=E5=8C=96=E2=80=9D?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E9=87=8C=E6=9C=89=E5=A4=AA=E5=A4=9A=E5=A4=9A?= =?UTF-8?q?=E7=9A=84=E8=A7=84=E5=88=99=E3=80=82=204.=E5=9C=A8=E9=98=85?= =?UTF-8?q?=E8=AF=BB=E7=95=8C=E9=9D=A2=EF=BC=8C=E4=BC=98=E5=8C=96=E9=95=BF?= =?UTF-8?q?=E6=8C=89=E6=8C=89=E9=92=AE=E2=80=9C=E6=9B=BF=E6=8D=A2=E2=80=9D?= =?UTF-8?q?=EF=BC=8C=E8=87=AA=E5=8A=A8=E6=B7=BB=E5=8A=A0=E4=B9=A6=E5=90=8D?= =?UTF-8?q?=E3=80=81=E7=BD=91=E5=9D=80=E5=88=B0=E5=BA=94=E7=94=A8=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=E4=B8=AD=E3=80=82=205.=E8=BF=9B=E4=B8=80=E6=AD=A5?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=87=AA=E5=8A=A8=E5=88=86=E6=AE=B5=E7=AE=97?= =?UTF-8?q?=E6=B3=95=206.=E9=95=BF=E6=8C=89=E9=80=89=E6=8B=A9=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E7=9A=84=E9=AB=98=E4=BA=AE=E8=89=B2=E5=9D=97=E7=9A=84?= =?UTF-8?q?=E5=BD=A2=E7=8A=B6=E7=94=B1=E5=BC=A7=E8=BE=B9=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=9C=86=E8=A7=92=E7=9F=A9=E5=BD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kunfei/bookshelf/bean/BookInfoBean.java | 2 +- .../bookshelf/bean/BookSource3Bean.java | 14 +- .../bookshelf/help/ChapterContentHelp.java | 645 ++++++++++++++++-- .../bookshelf/model/ReplaceRuleManager.java | 92 +++ .../presenter/BookSourcePresenter.java | 35 + .../contract/BookSourceContract.java | 1 + .../view/activity/BookDetailActivity.java | 14 + .../view/activity/BookSourceActivity.java | 3 + .../view/activity/ReadBookActivity.java | 72 +- .../view/popupwindow/ReadLongPressPop.java | 11 +- .../widget/modialog/ReplaceRuleDialog.java | 84 +++ .../widget/page/ChapterProvider.java | 9 +- .../bookshelf/widget/page/PageView.java | 4 +- .../main/res/drawable/ic_baseline_label.xml | 10 + .../main/res/layout/activity_book_read.xml | 2 +- .../main/res/layout/dialog_replace_rule.xml | 11 + .../main/res/layout/pop_read_long_press.xml | 49 +- .../res/menu/menu_book_source_activity.xml | 5 + app/src/main/res/values/ids.xml | 2 + app/src/main/res/values/strings.xml | 6 + 20 files changed, 973 insertions(+), 98 deletions(-) create mode 100644 app/src/main/res/drawable/ic_baseline_label.xml diff --git a/app/src/main/java/com/kunfei/bookshelf/bean/BookInfoBean.java b/app/src/main/java/com/kunfei/bookshelf/bean/BookInfoBean.java index 662eb644f6..9143a3a9cb 100644 --- a/app/src/main/java/com/kunfei/bookshelf/bean/BookInfoBean.java +++ b/app/src/main/java/com/kunfei/bookshelf/bean/BookInfoBean.java @@ -38,7 +38,7 @@ public class BookInfoBean implements Cloneable { private String chapterUrl; //章节目录地址,本地目录正则 private long finalRefreshData; //章节最后更新时间 private String coverUrl; //小说封面 - private String author;//作者 + private String author="";//作者 private String introduce; //简介 private String origin; //来源 private String charset;//编码 diff --git a/app/src/main/java/com/kunfei/bookshelf/bean/BookSource3Bean.java b/app/src/main/java/com/kunfei/bookshelf/bean/BookSource3Bean.java index 0e29790458..15c9790a23 100644 --- a/app/src/main/java/com/kunfei/bookshelf/bean/BookSource3Bean.java +++ b/app/src/main/java/com/kunfei/bookshelf/bean/BookSource3Bean.java @@ -143,7 +143,7 @@ private String searchUrl2RuleSearchUrl(String searchUrl) { Gson gson = new Gson(); httpRequest request = gson.fromJson(strings[1], httpRequest.class); if (gson.toJson(request).replaceAll("\\s", "").length() > 0) { - // 阅读2.0没有header,只有useragent + // 阅读2.0没有单独的header,只有useragent if (request.headers != null) { if (this.header == null) this.header = request.headers; @@ -208,6 +208,14 @@ public BookSourceBean toBookSourceBean() { ruleFindUrl=exploreUrl.replaceAll("\\{\\{page\\}\\}","searchPage"); } + // 暂时只给发现和搜索添加了header + String header=""; + if(this.header!=null){ + if(this.header.trim().length()>0) + header="@Header:"+this.header.replaceAll("\\n"," "); + } + + return new BookSourceBean( bookSourceUrl, bookSourceName, @@ -218,7 +226,7 @@ public BookSourceBean toBookSourceBean() { 0, //u serialNumber, weight, true, //u enable, - ruleFindUrl,//发现规则 ruleFindUrl, + ruleFindUrl+header,//发现规则 ruleFindUrl, ruleExplore.bookList, // 列表 ruleFindList, ruleExplore.name,// ruleFindName, ruleExplore.author,// ruleFindAuthor, @@ -227,7 +235,7 @@ public BookSourceBean toBookSourceBean() { ruleExplore.lastChapter,// ruleFindLastChapter, ruleExplore.coverUrl,// ruleFindCoverUrl, ruleExplore.bookUrl,//??? ruleFindNoteUrl, - RuleSearchUrl,// ruleSearchUrl, + RuleSearchUrl+header,// ruleSearchUrl, ruleSearch.bookList,// ruleSearchList, ruleSearch.name,// ruleSearchName, ruleSearch.author,// ruleSearchAuthor, diff --git a/app/src/main/java/com/kunfei/bookshelf/help/ChapterContentHelp.java b/app/src/main/java/com/kunfei/bookshelf/help/ChapterContentHelp.java index c0db306704..0cff54ffce 100644 --- a/app/src/main/java/com/kunfei/bookshelf/help/ChapterContentHelp.java +++ b/app/src/main/java/com/kunfei/bookshelf/help/ChapterContentHelp.java @@ -1,6 +1,7 @@ package com.kunfei.bookshelf.help; import android.text.TextUtils; +import android.util.Log; import com.kunfei.bookshelf.bean.ReplaceRuleBean; import com.kunfei.bookshelf.model.ReplaceRuleManager; @@ -8,8 +9,12 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class ChapterContentHelp { private static ChapterContentHelp instance; @@ -47,15 +52,435 @@ public String replaceContent(String bookName, String bookTag, String content, Bo //替换 for (ReplaceRuleBean replaceRule : ReplaceRuleManager.getEnabled()) { if (isUseTo(replaceRule.getUseTo(), bookTag, bookName)) { - try { - content = content.replaceAll(replaceRule.getFixedRegex(), replaceRule.getReplacement()); - } catch (Exception ignored) { + { + try { + // 因为这里获取不到context,就不使用getString(R.string.replace_ad)了 + if (replaceRule.getReplaceSummary().matches("^广告话术(-.*|$)")) { + // 跳过太短的文本 + if (content.length() > 100) + content = replaceAd2(content, replaceRule.getRegex()); + } else + content = content.replaceAll(replaceRule.getFixedRegex(), replaceRule.getReplacement()); + } catch (Exception e) { + e.printStackTrace(); + } } } } return toTraditional(content); } + // 緩存生成的廣告規則正則表達式 +// private Map adMap = new HashMap<>(); + private Map adMap = new HashMap<>(); + // 缓存长表达式,使用普通方式替换 + private Map adMapL = new HashMap<>(); + + // 使用广告话术规则对正文进行替换,此方法为正则算法,效率较高,但是有漏失,故暂时放弃使用 + private String replaceAd(String content, String replaceRule) { + // replaceRule只对选择的内容进行了切片,不包含正则 + + if (replaceRule == null) + return content; + + replaceRule = replaceRule.substring(2, replaceRule.length() - 2).trim(); + +// Pattern rule = adMap.get(replaceRule); + String rule = adMap.get(replaceRule); + StringBuffer buffer = new StringBuffer(replaceRule.length() * 2); + + if (rule == null) { + String rules[] = replaceRule.split("\n"); + + for (String s : rules) { + s = s.trim(); + if (s.length() < 1) + continue; + + // 如果规则只包含特殊字符,且长度大于2,直接替换。如果长度不大于2,会在自动扩大范围的过程中包含字符 + if (s.matches("\\p{P}*")) { + if (s.length() > 2) { + if (buffer.length() > 0) + buffer.append('|'); + buffer.append(Pattern.quote(s)); + } + } else { + // 如果规则不止包含特殊字符,需要移除首尾的特殊字符,把中间的空字符转换为\s+,把其他特殊字符转换为转义符 + if (buffer.length() > 0) + buffer.append('|'); + buffer.append(s + .replaceFirst("^\\p{P}+", "") + .replaceFirst("\\p{P}$", "") + .replaceAll("\\s+", "xxsp") + .replaceAll("(\\p{P})", "(\\\\p{P}?)") + .replaceAll("xxsp", "\\s+") + ); + } + } + // 广告话术至少出现两次 +// rule = Pattern.compile("((" + buffer + ")(\\p{P}{0,2})){1,10}(" + buffer + ")"); + rule = ("((" + buffer.toString() + ")(\\p{P}{0,2})){1,20}(" + buffer.toString() + ")((\\p{P}{0,12})(?=\\p{P}{2}))?"); + adMap.put(replaceRule, rule); + } + + content = content.replaceAll(rule, ""); + + rule = adMapL.get(replaceRule); + if (rule == null) { + String rules[] = replaceRule.split("\n"); + buffer = new StringBuffer(replaceRule.length() * 2); + + for (String s : rules) { + s = s.trim(); + if (s.length() < 1) + continue; + if (s.length() > 6) { + if (buffer.length() > 0) + buffer.append('|'); + buffer.append(Pattern.quote(s)); + } + } + rule = "(" + buffer.toString() + ")"; + adMapL.put(replaceRule, rule); + } +// Pattern p=Pattern.compile(rule); + content = content.replaceAll(rule, ""); + return content; + } + + + // 緩存生成的廣告 原文規則+正则扩展 + // 原文与正则的最大区别,在于正则匹配规则对特殊符号的处理是保守的 + private Map AdPatternMap = new HashMap<>(); + // 不包含符号的文本形式的规则缓存。用于广告规则的第二次替换,以解决如下问题: 规则有 abc def,而实际出现了adefbc + private Map AdStringDict = new HashMap<>(); + + // 使用广告话术规则对正文进行替换,此方法 使用Matcher匹配,合并相邻区域,再StringBlock.getResult()的算法取回没有被替换的部分 + // 广告话术规则的相关代码可能存在以下问题: 零宽断言书写错误, \p{P}的使用(比如我最开始不知道\p{P}是不包含\\s的), getResult.remove()的算法(为了方便调试专门写了verify方法) + private String replaceAd2(String content, String replaceRule) { + + if (replaceRule == null) + return content; + + StringBlock block = new StringBlock(content); + + Pattern rule = AdPatternMap.get(replaceRule); + String stringDict = AdStringDict.get(replaceRule); + + + if (rule == null) { + StringBuffer bufferRegex = new StringBuffer(replaceRule.length() * 3); + StringBuffer bufferDict = new StringBuffer(); + + String rules[] = replaceRule.split("\n"); + + for (String s : rules) { + s = s.trim(); + if (s.length() < 1) + continue; + + s = Pattern.quote(s); + + if (bufferRegex.length() > 0) + bufferRegex.append('|'); + else + bufferRegex.append("(?=("); + bufferRegex.append(s); + + } + + for (String s : rules) { + s = s.trim(); + if (s.length() < 1) + continue; + + // 如果规则不止包含特殊字符,需要移除首尾的特殊字符,把中间的空字符转换为\s+,把其他特殊字符转换为转义符 + if (!s.matches("[\\p{P}\\s]*")) { + if (bufferRegex.length() > 0) + bufferRegex.append('|'); + else + bufferRegex.append("(?=("); + bufferRegex.append(s + .replaceFirst("^\\p{P}+", "") + .replaceFirst("\\p{P}$", "") + .replaceAll("\\s+", "xxsp") + .replaceAll("(\\p{P})", "(\\\\p{P}?)") + .replaceAll("xxsp", "\\s+") + ); + } + if (s.matches("[\\p{P}\\s]*[^\\p{P}]{4,}[\\p{P}\\s]*")) { + bufferDict.append('\n'); + bufferDict.append(s); + } + } + bufferRegex.append("))((\\p{P}{0,12})(?=\\p{P}{2}))?"); + rule = Pattern.compile(bufferRegex.toString()); + AdPatternMap.put(replaceRule, rule); + stringDict = bufferDict.toString(); + AdStringDict.put(replaceRule, bufferDict.toString()); + } + + Matcher matcher0 = rule.matcher(content); + if (matcher0.groupCount() < 2) { +// 构造的正则表达式分2个部分,第一部分匹配文字,第二部分匹配符号。完成匹配后实际已经不需要拆墙了 + Log.w("replaceAd2", "2 > matcher0.group()==" + matcher0.groupCount()); + return content; + } + + while (matcher0.find()) { + + if (matcher0.group(2) != null) + block.remove(matcher0.start(), matcher0.start() + matcher0.group(1).length() + matcher0.group(2).length()); + else + block.remove(matcher0.start(), matcher0.start() + matcher0.group(1).length()); + + Log.d("replaceAd2()", "Remove=" + block.verify()); + } + block.remove("(\\p{P}|\\s){1,6}([^\\p{P}]?(\\p{P}|\\s){1,6})?"); + block.removeDict(stringDict); + block.increase(5); + return block.getResult(); + } + + + class StringBlock { + // 保存字符串本体 + private String string = ""; + // 保存可以复制的区域,奇数为start,偶数为end。 + private ArrayList list; + // 保存删除的区域,用于校验 + private ArrayList removed; + + public StringBlock(String string) { + this.string = string; + list = new ArrayList<>(); + list.add(0); + list.add(string.length()); + removed = new ArrayList<>(); + } + + // 验证删除操作是否有bug 验证OK输出正数,异常输出负数 + public int verify() { + // 验证list数列是否有异常 + if (list.size() % 2 != 0) + return -1; + + int p = list.get(0); + if (p < 0) + return -2; + for (int i = 1; i < list.size(); i++) { + int q = list.get(i); + if (q <= p) + return -3; + p = q; + } + // 验证删除的区域是否还在list构成的区域内 + for (int j = 0; j < removed.size() / 2; j++) { + int j2 = removed.get(j * 2); + int j2_1 = removed.get(j * 2 + 1); + for (int i = 0; i < list.size() / 2; i++) { + int i2_1 = list.get(i * 2 + 1); + int i2 = list.get(i * 2); + if (i2 > j2) { + break; + } + if (i2_1 < j2) { + continue; + } + + if (i2_1 == j2) { + if (i * 2 + 2 < list.size()) { + if (list.get(i * 2 + 2) < j2_1) + return -4; + } + } else { + return -5; + } + + } + } + + return 0; + } + + // 增加字符串的文本,避免被误删除 + public void increase(int size) { + ArrayList cache = new ArrayList<>(); + if (list.get(0) > size) + cache.add(list.get(0)); + else + cache.add(0); + for (int i = 1; i < list.size() - 1; i = i + 2) { + if (list.get(i + 1) - list.get(i) > size) { + cache.add(list.get(i)); + cache.add(list.get(i + 1)); + } + } + if (string.length() - list.get(list.size() - 1) > size) + cache.add(list.get(list.size() - 1)); + else + cache.add(string.length()); + list = cache; + } + + // 去除长度小于等于墙厚的区域 + public void remove(int wallThick) { + int j = list.size() / 2; + ArrayList cache = new ArrayList<>(); + for (int i = 0; i < j; i++) { + int i2_1 = list.get(i * 2 + 1); + int i2 = list.get(i * 2); + if ((i2_1 - i2) > wallThick) { + cache.add(i2); + cache.add(i2_1); + } + } + list = cache; + } + + // 去除完全与正则匹配的区域 + public void remove(String wall) { + int j = list.size() / 2; + ArrayList cache = new ArrayList<>(); + for (int i = 0; i < j; i++) { + int i2_1 = list.get(i * 2 + 1); + int i2 = list.get(i * 2); + if (!string.substring(i2, i2_1).matches(wall)) { + cache.add(i2); + cache.add(i2_1); + } + } + list = cache; + } + + public void removeDict(String dict) { +// 如果孔穴的两端刚好匹配到同一词条,说明这是嵌套的广告话术 + int j = list.size() / 2; + // 缓存需要操作的参数 + ArrayList cache = new ArrayList<>(); + for (int i = 1; i < j; i++) { + + String str_s0 = getSubString(2 * i - 2).replaceFirst("[\\p{P}\\s]+$", ""); + + String str_s1 = str_s0.replaceFirst("^.*[\\p{P}\\s][^$]", ""); + if (str_s1.length() < 1) + continue; + + String str_e0 = getSubString(2 * i).replaceFirst("^[\\p{P}\\s]+", ""); + String str_e1 = str_e0.replaceFirst("[\\p{P}\\s].*$", ""); + if (str_e1.length() < 1) + continue; + + + // m 第一部分开始的位置 + int m = list.get(i * 2 - 2) + str_s0.length() - str_s1.length(); + // 第二部分结尾 + int n = list.get(i * 2 + 1) - str_e0.length() + str_e1.length(); + + if (dict.matches("[\\s\\S]*(" + str_s1 + ")([^\\p{P}]*)(" + str_e1 + ")[\\s\\S]*")) { + cache.add(m); + cache.add(n); + } else if (dict.matches("[\\s\\S]*(\n|^).*" + str_s1 + ".*(\n|\\s*$)[\\s\\S]*")) { + cache.add(m); + cache.add(list.get(i * 2)); + } else if (dict.matches("[\\s\\S]*(\n|^).*" + str_e1 + ".*(\n|\\s*$)[\\s\\S]*")) { + // 因为java.*不匹配\n + cache.add(list.get(i * 2)); + cache.add(n); + } + } + + for (int i = 0; i < cache.size() / 2; i++) { + Log.d("removeDict", string.substring(cache.get(i * 2), cache.get((i * 2 + 1)))); + remove(cache.get(i * 2), cache.get((i * 2 + 1))); + } + + } + + public boolean remove(int start, int end) { + if (start < 0 || end < 0 || start > string.length() || end > string.length() || start >= end) + return false; + + removed.add(start); + removed.add(end); + + int j = list.size() / 2; + for (int i = 0; i < j; i++) { +// start在有效区间中间和在区间的两个边缘,是不同的算法。 + int i2_1 = list.get(i * 2 + 1); + int i2 = list.get(i * 2); + + if (start < i2) + return true; + + if (start == i2) { + if (i2_1 > end) { + list.set(i * 2, end); + return true; + } else { + for (int k = 0; 2 * i + k < list.size(); k++) { + if (list.get(k + 2 * i) > end) { + if (k % 2 == 1) { + list.set(2 * i + k - 1, end); + } else { + list.remove(i * 2); + } + for (int m = 0; m < k - 1; m++) + list.remove(i * 2); + return true; + } + } + } + } else if (i2 < start && i2_1 > start) { + if (i2_1 > end) { + list.add(i * 2 + 1, end); + list.add(i * 2 + 1, start); + return true; + } else { + list.set(i * 2 + 1, start); + // i*2+2开始的元素可能需要被删除 + for (int k = 2; 2 * i + k < list.size(); k++) { + if (list.get(k + 2 * i) < end) + continue; + + if (k % 2 == 1) { + if (list.get(k + 2 * i) > end) { + list.set(2 * i + k - 1, end); + } + } else { + list.remove(i * 2 + 2); + } + + for (int m = 0; m < k - 1; m++) + list.remove(i * 2 + 2); + return true; + } + } + } + } + + return false; + + } + + public String getResult() { + StringBuffer buffer = new StringBuffer(string.length()); + int j = list.size() / 2; + if (j * 2 > list.size()) + Log.e("StringBlock", "list.size=" + list.size()); + for (int i = 0; i < j; i++) { + buffer.append(string, list.get(i * 2), list.get(i * 2 + 1)); + } + return buffer.toString(); + } + + public String getSubString(int start) { + if (start >= 0 && start < list.size() - 1) + return string.substring(list.get(start), list.get(start + 1)); + return null; + } + } + /** * 段落重排算法入口。把整篇内容输入,连接错误的分段,再把每个段落调用其他方法重新切分 * @@ -78,9 +503,11 @@ public static String LightNovelParagraph2(String content, String chapterName) { _content = content; } + List dict = makeDict(_content); + String[] p = _content .replaceAll(""", "“") - .replaceAll("[::]['\"‘”“]+",":“") + .replaceAll("[::]['\"‘”“]+", ":“") .replaceAll("[\"”“]+[\\s]*[\"”“][\\s\"”“]*", "”\n“") .split("\n(\\s*)"); @@ -89,7 +516,7 @@ public static String LightNovelParagraph2(String content, String chapterName) { // 章节的文本格式为章节标题-空行-首段,所以处理段落时需要略过第一行文本。 buffer.append(" "); - if (!chapterName.trim().equals(p[0].trim())){ + if (!chapterName.trim().equals(p[0].trim())) { // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内 buffer.append(p[0].replaceAll("[\u3000\\s]+", "")); } @@ -119,7 +546,7 @@ public static String LightNovelParagraph2(String content, String chapterName) { for (String s : p) { buffer.append("\n"); - buffer.append(FindNewLines(s) + buffer.append(FindNewLines(s, dict) ); } @@ -135,14 +562,53 @@ public static String LightNovelParagraph2(String content, String chapterName) { // 而规范使用的标点不会被误处理: “你”、“我”、“他”,都是一样的。 .replaceAll("\\s*[\"”“]+[\\s]*[\"”“][\\s\"”“]*", "”\n“") // 规范 A:“B... - .replaceAll("[::][”“\"\\s]+",":“") + .replaceAll("[::][”“\"\\s]+", ":“") // 处理奇怪的多余引号 \n”A:“B... 为 \nA:“B... - .replaceAll("\n[\"“”]([^\n\"“”]+)([,:,:][\"”“])([^\n\"“”]+)","\n$1:“$3") - .replaceAll("\n(\\s*)", "\n"); + .replaceAll("\n[\"“”]([^\n\"“”]+)([,:,:][\"”“])([^\n\"“”]+)", "\n$1:“$3") + .replaceAll("\n(\\s*)", "\n") + // 处理“……” +// .replaceAll("\n[\"”“][.,。,…]+\\s*[.,。,…]+[\"”“]","\n“……”") + // 处理被错误断行的省略号。存在较高的误判,但是我认为利大于弊 + .replaceAll("[.,。,…]+\\s*[.,。,…]+", "……") + .replaceAll("\n([\\s::,,]+)", "\n") + ; } return content; } + /** + * 从字符串提取引号包围,且不止出现一次的内容为字典 + * + * @param str + * @return 词条列表 + */ + private static List makeDict(String str) { + + // 引号中间不包含任何标点 + Pattern patten = Pattern.compile("(?<=[\"'”“])([^\n\\p{P}]{1," + WORD_MAX_LENGTH + "})(?=[\"'”“])"); +// Pattern patten = Pattern.compile("(?<=[\"'”“])([^\n\"'”“]{1,16})(?=[\"'”“])"); + Matcher matcher = patten.matcher(str); + + List cache = new ArrayList<>(); + List dict = new ArrayList<>(); + + while (matcher.find()) { + String word = matcher.group(); + if (cache.contains(word)) { + if (!dict.contains(word)) + dict.add(word); + } else + cache.add(word); + } +/* + System.out.print("makeDict:"); + for (String s : dict) + System.out.print("\t" + s); + System.out.print("\n"); + */ + return dict; + } + /** * 强制切分,减少段落内的句子 * 如果连续2对引号的段落没有提示语,进入对话模式。最后一对引号后强制切分段落 @@ -202,12 +668,12 @@ private static String splitQuote(String str) { if (match(MARK_QUOTATION, str.charAt(0))) { int i = seekIndex(str, MARK_QUOTATION, 1, length - 2, true) + 1; if (i > 1) - if(!match(MARK_QUOTATION_BEFORE,str.charAt(i-1))) + if (!match(MARK_QUOTATION_BEFORE, str.charAt(i - 1))) return str.substring(0, i) + "\n" + str.substring(i); } else if (match(MARK_QUOTATION, str.charAt(length - 1))) { int i = length - 1 - seekIndex(str, MARK_QUOTATION, 1, length - 2, false); if (i > 1) - if(!match(MARK_QUOTATION_BEFORE,str.charAt(i-1))) + if (!match(MARK_QUOTATION_BEFORE, str.charAt(i - 1))) return str.substring(0, i) + "\n" + str.substring(i); } return str; @@ -215,38 +681,41 @@ private static String splitQuote(String str) { /** * 计算随机插入换行符的位置。 - * @param str 字符串 + * + * @param str 字符串 * @param offset 传回的结果需要叠加的偏移量 - * @param min 最低几个句子,随机插入换行 - * @param gain 倍率。每个句子插入换行的数学期望 = 1 / gain , gain越大越不容易插入换行 + * @param min 最低几个句子,随机插入换行 + * @param gain 倍率。每个句子插入换行的数学期望 = 1 / gain , gain越大越不容易插入换行 * @return */ - private static ArrayList forceSplit(String str,int offset,int min,int gain,int tigger) { - ArrayList result=new ArrayList<>(); - ArrayList array_end=seekIndexs(str,MARK_SENTENCES_END_P,0,str.length()-2,true); - ArrayList array_mid=seekIndexs(str,MARK_SENTENCES_MID,0,str.length()-2,true); - if(array_end.size() forceSplit(String str, int offset, int min, int gain, int tigger) { + ArrayList result = new ArrayList<>(); + ArrayList array_end = seekIndexs(str, MARK_SENTENCES_END_P, 0, str.length() - 2, true); + ArrayList array_mid = seekIndexs(str, MARK_SENTENCES_MID, 0, str.length() - 2, true); + if (array_end.size() < tigger && array_mid.size() < tigger * 3) return result; - int j=0; - for(int i=min;i dict) { StringBuffer string = new StringBuffer(str); // 标记string中每个引号的位置.特别的,用引号进行列举时视为只有一对引号。 如:“锅”、“碗”视为“锅、碗”,从而避免误断句。 List array_quote = new ArrayList<>(); + // 标记忽略的引号 + List array_ignore_quote = new ArrayList<>(); // 标记插入换行符的位置,int为插入位置(str的char下标) ArrayList ins_n = new ArrayList<>(); @@ -263,11 +732,20 @@ private static String FindNewLines(String str) { if (match(MARK_QUOTATION, c)) { int size = array_quote.size(); - // 把“xxx”、“yy”合并为“xxx_yy”进行处理 + // 把“xxx”、“yy”和“z”合并为“xxx_yy_z”进行处理 if (size > 0) { int quote_pre = array_quote.get(size - 1); if (i - quote_pre == 2) { - if (match(",、,/", str.charAt(i - 1))) { + boolean remove = false; + if (wait_close) { + if (match(",,、/", str.charAt(i - 1))) { + // 考虑出现“和”这种特殊情况 + remove = true; + } + } else if (match(",,、/和与或", str.charAt(i - 1))) { + remove = true; + } + if (remove) { string.setCharAt(i, '“'); string.setCharAt(i - 2, '”'); array_quote.remove(size - 1); @@ -282,26 +760,29 @@ private static String FindNewLines(String str) { // 为xxx:“xxx”做标记 if (i > 1) { // 当前发言的正引号的前一个字符 - char char_b1=str.charAt(i - 1); + char char_b1 = str.charAt(i - 1); // 上次发言的正引号的前一个字符 - char char_b2=0; + char char_b2 = 0; if (match(MARK_QUOTATION_BEFORE, char_b1)) { // 如果不是第一处引号,寻找上一处断句,进行分段 if (array_quote.size() > 1) { int last_quote = array_quote.get(array_quote.size() - 2); - int p=0; - if(char_b1==',' || char_b1==','){ - if(array_quote.size()>2){ - p=array_quote.get(array_quote.size()-3); - if(p>0){ - char_b2=str.charAt(p-1); + int p = 0; + if (char_b1 == ',' || char_b1 == ',') { + if (array_quote.size() > 2) { + p = array_quote.get(array_quote.size() - 3); + if (p > 0) { + char_b2 = str.charAt(p - 1); } } } // if(char_b2=='.' || char_b2=='。') - if(match(MARK_SENTENCES_END_P,char_b2)) - ins_n.add(p-1); - else{ + if (match(MARK_SENTENCES_END_P, char_b2)) + ins_n.add(p - 1); + else if (match("的", char_b2)) { + //剔除引号标记aaa的"xxs",bbb的“yyy” + + } else { int last_end = seekLast(str, MARK_SENTENCES_END, i, last_quote); if (last_end > 0) ins_n.add(last_end); @@ -372,7 +853,7 @@ private static String FindNewLines(String str) { if (opend) { if (array_quote.get(size - 1) - string.length() > -3) { // if((match(MARK_QUOTATION,string.charAt(string.length()-1)) || match(MARK_QUOTATION,string.charAt(string.length()-2)))){ - if(size>1) + if (size > 1) mod[size - 2] = 4; // 0<=i=1 mod[size - 1] = -4; @@ -413,13 +894,43 @@ private static String FindNewLines(String str) { // "xxxx" xxxx。\n xxx“xxxx” // 未实现 + + // 使用字典验证ins_n , 避免插入不必要的换行。 + // 由于目前没有插入、的列表,无法解决 “xx”、“xx”“xx” 被插入换行的问题 + ArrayList _ins_n = new ArrayList<>(); + for (int i : ins_n) { + if (match("\"'”“", string.charAt(i))) { + int start = seekLast(str, "\"'”“", i - 1, i - WORD_MAX_LENGTH); + if (start > 0) { + String word = str.substring(start + 1, i); + + if (dict.contains(word)) { +// System.out.println("使用字典验证 跳过\tins_n=" + i + " word=" + word); +// 引号内如果是字典词条,后方不插入换行符(前方不需要优化) + continue; + } else { +// System.out.println("使用字典验证 插入\tins_n=" + i + " word=" + word); + if (match("的地得", str.charAt(start))) { +// xx的“xx”,后方不插入换行符(前方不需要优化) + continue; + } + + } + } + } + _ins_n.add(i); + } + ins_n = _ins_n; + + // 随机在句末插入换行符 ins_n = new ArrayList(new HashSet(ins_n)); Collections.sort(ins_n); + { - String subs=""; - int j=0; + String subs = ""; + int j = 0; int progress = 0; int next_line = -1; @@ -427,19 +938,19 @@ private static String FindNewLines(String str) { next_line = ins_n.get(j); int gain = 3; - int min=0; - int trigger=2; + int min = 0; + int trigger = 2; for (int i = 0; i < array_quote.size(); i++) { int qutoe = array_quote.get(i); - if(qutoe>0){ - gain=4; - min=2; - trigger=4; - }else{ + if (qutoe > 0) { + gain = 4; + min = 2; + trigger = 4; + } else { gain = 3; - min=0; - trigger=2; + min = 0; + trigger = 2; } // 把引号前的换行符与内容相间插入 @@ -448,15 +959,15 @@ private static String FindNewLines(String str) { if (next_line >= qutoe) break; next_line = ins_n.get(j); - if(progress seekIndexs(String str, String key, int from, i * * @param str 数据字符串 * @param key 字典字符串 - * @param from 从哪个字符开始匹配,默认0 - * @param to 匹配到哪个字符(不包含此字符)默认匹配到最末位 + * @param from 从哪个字符开始匹配,默认最末位 + * @param to 匹配到哪个字符(不包含此字符)默认0 * @return 位置(正向计算) */ private static int seekLast(String str, String key, int from, int to) { if (str.length() - from < 1) return -1; - int i = 0; - if (from > i) + int i = str.length() - 1; + if (from < i && i > 0) i = from; int t = 0; if (to > 0) @@ -738,6 +1249,8 @@ private static int seekWordsIndex(String str, int form, int to, boolean inOrder, private static String MARK_QUOTATION = "\"“”"; private static String PARAGRAPH_DIAGLOG = "^[\"”“][^\"”“]+[\"”“]$"; + // 限制字典的长度 + private static int WORD_MAX_LENGTH = 16; private static boolean isFullSentences(String s) { if (s.length() < 2) diff --git a/app/src/main/java/com/kunfei/bookshelf/model/ReplaceRuleManager.java b/app/src/main/java/com/kunfei/bookshelf/model/ReplaceRuleManager.java index 310e012337..325a04126e 100644 --- a/app/src/main/java/com/kunfei/bookshelf/model/ReplaceRuleManager.java +++ b/app/src/main/java/com/kunfei/bookshelf/model/ReplaceRuleManager.java @@ -13,6 +13,9 @@ import com.kunfei.bookshelf.utils.RxUtils; import com.kunfei.bookshelf.utils.StringUtils; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import io.reactivex.Observable; @@ -38,6 +41,95 @@ public static List getEnabled() { return replaceRuleBeansEnabled; } + // 合并广告话术规则 + public static Single mergeAdRules(ReplaceRuleBean replaceRuleBean) { + + + String rule = formateAdRule(replaceRuleBean.getRegex()); + +/* String summary=replaceRuleBean.getReplaceSummary(); + if(summary==null) + summary=""; + String sumary_pre=summary.split("-")[0];*/ + + int sn = replaceRuleBean.getSerialNumber(); + if (sn == 0) { + sn = (int) (DbHelper.getDaoSession().getReplaceRuleBeanDao().queryBuilder().count() + 1); + replaceRuleBean.setSerialNumber(sn); + } + + List list = DbHelper.getDaoSession() + .getReplaceRuleBeanDao().queryBuilder() + .where(ReplaceRuleBeanDao.Properties.Enable.eq(true)) + .where(ReplaceRuleBeanDao.Properties.ReplaceSummary.eq(replaceRuleBean.getReplaceSummary())) + .where(ReplaceRuleBeanDao.Properties.SerialNumber.notEq(sn)) + .orderAsc(ReplaceRuleBeanDao.Properties.SerialNumber) + .list(); + if (list.size() < 1) { + replaceRuleBean.setRegex(rule); + return saveData(replaceRuleBean); + } else { + StringBuffer buffer = new StringBuffer(rule); + for (ReplaceRuleBean li : list) { + buffer.append('\n'); + buffer.append(li.getRegex()); +// buffer.append(formateAdRule(rule.getRegex())); + } + replaceRuleBean.setRegex(formateAdRule(buffer.toString())); + + return Single.create((SingleOnSubscribe) emitter -> { + + DbHelper.getDaoSession().getReplaceRuleBeanDao().insertOrReplace(replaceRuleBean); + for (ReplaceRuleBean li : list) { + DbHelper.getDaoSession().getReplaceRuleBeanDao().delete(li); + } + refreshDataS(); + emitter.onSuccess(true); + }).compose(RxUtils::toSimpleSingle); + + } + } + + // 把输入的规则进行预处理(分段、排序、去重)。保存的是普通多行文本。 + public static String formateAdRule(String rule) { + + if (rule == null) + return ""; + String result = rule.trim(); + if (result.length() < 1) + return ""; + + String string = rule +// 用中文中的.视为。进行分段 + .replaceAll("(?<=([^a-zA-Z\\p{P}]{4,8}))\\.+(?![^a-zA-Z\\p{P}]{4,8})","\n") +// 用常见的适合分段的标点进行分段,句首句尾除外 +// .replaceAll("([^\\p{P}\n^])([…,,::?。!?!~<>《》【】()()]+)([^\\p{P}\n$])", "$1\n$3") +// 表达式无法解决句尾连续多个符号的问题 +// .replaceAll("[…,,::?。!?!~<>《》【】()()]+(?!\\s*\n|$)", "\n") + .replaceAll("(?《》【】()()]+)(?![\\p{P}\n$])", "\n") + + ; + + String[] lines = string.split("\n"); + List list = new ArrayList<>(); + + for (String s : lines) { + s = s.trim() +// .replaceAll("\\s+", "\\s") + ; + if (!list.contains(s)) { + list.add(s); + } + } + Collections.sort(list); + StringBuffer buffer = new StringBuffer(rule.length() + 1); + for (int i = 0; i < list.size(); i++) { + buffer.append('\n'); + buffer.append(list.get(i)); + } + return buffer.toString().trim(); + } + public static Single> getAll() { return Single.create((SingleOnSubscribe>) emitter -> emitter.onSuccess(DbHelper.getDaoSession() .getReplaceRuleBeanDao().queryBuilder() diff --git a/app/src/main/java/com/kunfei/bookshelf/presenter/BookSourcePresenter.java b/app/src/main/java/com/kunfei/bookshelf/presenter/BookSourcePresenter.java index fee2385fe4..b56307b70a 100644 --- a/app/src/main/java/com/kunfei/bookshelf/presenter/BookSourcePresenter.java +++ b/app/src/main/java/com/kunfei/bookshelf/presenter/BookSourcePresenter.java @@ -191,6 +191,41 @@ public void checkBookSource(List sourceBeans) { CheckSourceService.start(mView.getContext(), sourceBeans); } + @Override + public void checkFindSource(List sourceBeans) { + String TAG_FIND_SOUECE="发现"; + + Observable.create((ObservableOnSubscribe) e -> { + for (BookSourceBean sourceBean : sourceBeans) { + String rule=sourceBean.getRuleFindUrl(); + if(rule==null) + sourceBean.removeGroup(TAG_FIND_SOUECE); + else if(rule.trim().length()<1){ + sourceBean.removeGroup(TAG_FIND_SOUECE); + sourceBean.setRuleFindUrl(null); + }else{ + sourceBean.addGroup(TAG_FIND_SOUECE); + } + DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplaceInTx(sourceBean); + } + e.onNext(true); + }).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new MyObserver() { + @Override + public void onNext(Boolean aBoolean) { + mView.toast(TAG_FIND_SOUECE+"标签验校完成"); + mView.refreshBookSource(); + mView.setResult(RESULT_OK); + } + + @Override + public void onError(Throwable e) { + mView.toast("验校失败"); + } + }); + } + ///////////////////////////////////////////////// @Override diff --git a/app/src/main/java/com/kunfei/bookshelf/presenter/contract/BookSourceContract.java b/app/src/main/java/com/kunfei/bookshelf/presenter/contract/BookSourceContract.java index 2211b9138d..ac354ee782 100644 --- a/app/src/main/java/com/kunfei/bookshelf/presenter/contract/BookSourceContract.java +++ b/app/src/main/java/com/kunfei/bookshelf/presenter/contract/BookSourceContract.java @@ -25,6 +25,7 @@ interface Presenter extends IPresenter { void checkBookSource(List sourceBeans); + void checkFindSource(List sourceBeans); } interface View extends IView { diff --git a/app/src/main/java/com/kunfei/bookshelf/view/activity/BookDetailActivity.java b/app/src/main/java/com/kunfei/bookshelf/view/activity/BookDetailActivity.java index 33f06e0538..9daa9036d1 100644 --- a/app/src/main/java/com/kunfei/bookshelf/view/activity/BookDetailActivity.java +++ b/app/src/main/java/com/kunfei/bookshelf/view/activity/BookDetailActivity.java @@ -2,6 +2,9 @@ package com.kunfei.bookshelf.view.activity; import android.annotation.SuppressLint; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; @@ -434,6 +437,9 @@ public void onSuccess(Bitmap bitmap2) { if (!mPresenter.getBookShelf().getTag().equals(BookShelfBean.LOCAL_TAG)) { popupMenu.getMenu().add(Menu.NONE, R.id.menu_edit, Menu.NONE, R.string.edit_book_source); } + if (!mPresenter.getBookShelf().getTag().equals(BookShelfBean.LOCAL_TAG)) { + popupMenu.getMenu().add(Menu.NONE, R.id.menu_copy_url, Menu.NONE, R.string.copy_url); + } popupMenu.setOnMenuItemClickListener(menuItem -> { switch (menuItem.getItemId()) { case R.id.menu_refresh: @@ -453,6 +459,14 @@ public void onSuccess(Bitmap bitmap2) { SourceEditActivity.startThis(this, sourceBean); } break; + case R.id.menu_copy_url: + ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clipData = ClipData.newPlainText(null, mPresenter.getBookShelf().getNoteUrl()); + if (clipboard != null) { + clipboard.setPrimaryClip(clipData); + toast(R.string.copy_complete); + } + break; } return true; }); diff --git a/app/src/main/java/com/kunfei/bookshelf/view/activity/BookSourceActivity.java b/app/src/main/java/com/kunfei/bookshelf/view/activity/BookSourceActivity.java index 172da636bb..67c8f1190d 100644 --- a/app/src/main/java/com/kunfei/bookshelf/view/activity/BookSourceActivity.java +++ b/app/src/main/java/com/kunfei/bookshelf/view/activity/BookSourceActivity.java @@ -301,6 +301,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.action_check_book_source: mPresenter.checkBookSource(adapter.getSelectDataList()); break; + case R.id.action_check_find_source: + mPresenter.checkFindSource(adapter.getSelectDataList()); + break; case R.id.sort_manual: upSourceSort(0); break; diff --git a/app/src/main/java/com/kunfei/bookshelf/view/activity/ReadBookActivity.java b/app/src/main/java/com/kunfei/bookshelf/view/activity/ReadBookActivity.java index 92e4465b80..fffa43266c 100644 --- a/app/src/main/java/com/kunfei/bookshelf/view/activity/ReadBookActivity.java +++ b/app/src/main/java/com/kunfei/bookshelf/view/activity/ReadBookActivity.java @@ -1,6 +1,8 @@ //Copyright (c) 2017. 章钦豪. All rights reserved. package com.kunfei.bookshelf.view.activity; +import java.util.regex.Matcher; + import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.ClipData; @@ -16,6 +18,7 @@ import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; +import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; @@ -94,6 +97,7 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.regex.Pattern; import butterknife.BindView; import butterknife.ButterKnife; @@ -1037,14 +1041,14 @@ public void showAction(View clickView) { readLongPress.setVisibility(View.VISIBLE); //如果太靠右,则靠左 int[] aa = ScreenUtils.getScreenSize(this); - if ((cursorLeft.getX() + ScreenUtils.dpToPx(120)) > aa[0]) { - readLongPress.setX(cursorLeft.getX() - ScreenUtils.dpToPx(125)); + if ((cursorLeft.getX() + ScreenUtils.dpToPx(200)) > aa[0]) { + readLongPress.setX(aa[0] - ScreenUtils.dpToPx(200)); } else { readLongPress.setX(cursorLeft.getX() + cursorLeft.getWidth() + ScreenUtils.dpToPx(5)); } //如果太靠上 - if ((cursorLeft.getY() - ScreenUtils.spToPx(readBookControl.getTextSize()) - ScreenUtils.dpToPx(40)) < 0) { + if ((cursorLeft.getY() - ScreenUtils.spToPx(readBookControl.getTextSize()) - ScreenUtils.dpToPx(60)) < 0) { readLongPress.setY(cursorLeft.getY() - ScreenUtils.spToPx(readBookControl.getTextSize())); } else { readLongPress.setY(cursorLeft.getY() - ScreenUtils.spToPx(readBookControl.getTextSize()) - ScreenUtils.dpToPx(40)); @@ -1111,9 +1115,9 @@ public void replaceSelect() { oldRuleBean.setIsRegex(false); oldRuleBean.setReplacement(""); oldRuleBean.setSerialNumber(0); - oldRuleBean.setUseTo(""); + oldRuleBean.setUseTo(String.format("%s,%s", mPresenter.getBookShelf().getBookInfoBean().getName(), mPresenter.getBookShelf().getTag())); - ReplaceRuleDialog.builder(ReadBookActivity.this, oldRuleBean, mPresenter.getBookShelf()) + ReplaceRuleDialog.builder(ReadBookActivity.this, oldRuleBean, mPresenter.getBookShelf(), ReplaceRuleDialog.DefaultUI) .setPositiveButton(replaceRuleBean1 -> ReplaceRuleManager.saveData(replaceRuleBean1) .subscribe(new MySingleObserver() { @@ -1130,6 +1134,64 @@ public void onSuccess(Boolean aBoolean) { refresh(false); } })).show(); + } + + @Override + public void replaceSelectAd() { + String selectString = pageView.getSelectStr(); + + + if (selectString != null) { + String spacer = null; + String name = (mPresenter.getBookShelf().getBookInfoBean().getName()); + if (name != null) + if (name.trim().length() > 0) + spacer = "|" + Pattern.quote(name.trim()); +// spacer = "|" + Matcher.quoteReplacement(name.trim()); + + name = (mPresenter.getBookShelf().getBookInfoBean().getAuthor()); + if (name != null) + if (name.trim().length() > 0) + if (spacer != null) + spacer = spacer + "|" + Pattern.quote(name.trim()); + else + spacer = "|" + Pattern.quote(name.trim()); + String rule="(\\s*\n\\s*" + spacer + ")"; + selectString = ReplaceRuleManager.formateAdRule( + selectString.replaceAll(rule, "\n") + ); + + Log.i("selectString.afterAd2",selectString); + + } + + + ReplaceRuleBean oldRuleBean = new ReplaceRuleBean(); + oldRuleBean.setReplaceSummary(getString(R.string.replace_ad) + "-" + mPresenter.getBookShelf().getTag()); + oldRuleBean.setEnable(true); + oldRuleBean.setRegex(selectString); + oldRuleBean.setIsRegex(false); + oldRuleBean.setReplacement(""); + oldRuleBean.setSerialNumber(0); + oldRuleBean.setUseTo(String.format(mPresenter.getBookShelf().getTag())); + + ReplaceRuleDialog.builder(ReadBookActivity.this, oldRuleBean, mPresenter.getBookShelf(), ReplaceRuleDialog.AddAdUI) + .setPositiveButton(replaceRuleBean1 -> + ReplaceRuleManager.mergeAdRules(replaceRuleBean1) + .subscribe(new MySingleObserver() { + @Override + public void onSuccess(Boolean aBoolean) { + cursorLeft.setVisibility(View.INVISIBLE); + cursorRight.setVisibility(View.INVISIBLE); + readLongPress.setVisibility(View.INVISIBLE); + + pageView.setSelectMode(PageView.SelectMode.Normal); + + moDialogHUD.dismiss(); + + refresh(false); + } + })).show(); } }); diff --git a/app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadLongPressPop.java b/app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadLongPressPop.java index 806546c95e..e1a384c7d7 100644 --- a/app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadLongPressPop.java +++ b/app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadLongPressPop.java @@ -28,6 +28,8 @@ public class ReadLongPressPop extends FrameLayout { FrameLayout flReplace; @BindView(R.id.fl_cp) FrameLayout flCp; + @BindView(R.id.fl_replace_ad) + FrameLayout flReplaceAd; //private ReadBookActivity activity; @@ -82,11 +84,14 @@ private void initData() { private void bindEvent() { - //翻页1 + //复制 flCp.setOnClickListener(v -> clickListener.copySelect()); - //翻页2 + //替换 flReplace.setOnClickListener(v -> clickListener.replaceSelect()); + + //标记广告 + flReplaceAd.setOnClickListener(v -> clickListener.replaceSelectAd()); } public interface OnBtnClickListener { @@ -94,5 +99,7 @@ public interface OnBtnClickListener { void replaceSelect(); + void replaceSelectAd(); + } } \ No newline at end of file diff --git a/app/src/main/java/com/kunfei/bookshelf/widget/modialog/ReplaceRuleDialog.java b/app/src/main/java/com/kunfei/bookshelf/widget/modialog/ReplaceRuleDialog.java index 38c0455c98..f8d07d1391 100644 --- a/app/src/main/java/com/kunfei/bookshelf/widget/modialog/ReplaceRuleDialog.java +++ b/app/src/main/java/com/kunfei/bookshelf/widget/modialog/ReplaceRuleDialog.java @@ -3,8 +3,10 @@ import android.annotation.SuppressLint; import android.content.Context; import android.text.Editable; +import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; +import android.view.inputmethod.EditorInfo; import android.widget.CheckBox; import android.widget.TextView; @@ -27,6 +29,20 @@ public class ReplaceRuleDialog extends BaseDialog { private ReplaceRuleBean replaceRuleBean; private BookShelfBean bookShelfBean; + private TextView replace_ad_intro, tvtitle; + private View til_replace_to; + + // 替换规则编辑UI的模式 1 默认 2 广告话术 3 添加广告话术 + private int ReplaceUIMode = 1; + public static int DefaultUI = 1, AdUI = 2, AddAdUI = 3; + private String str_summary = ""; + + + public static ReplaceRuleDialog builder(Context context, ReplaceRuleBean replaceRuleBean, BookShelfBean bookShelfBean, int replaceUIMode) { + return new ReplaceRuleDialog(context, replaceRuleBean, bookShelfBean, replaceUIMode); + } + + public static ReplaceRuleDialog builder(Context context, ReplaceRuleBean replaceRuleBean, BookShelfBean bookShelfBean) { return new ReplaceRuleDialog(context, replaceRuleBean, bookShelfBean); } @@ -37,6 +53,19 @@ private ReplaceRuleDialog(Context context, ReplaceRuleBean replaceRuleBean, Book this.replaceRuleBean = replaceRuleBean; this.bookShelfBean = bookShelfBean; + @SuppressLint("InflateParams") View view = LayoutInflater.from(context).inflate(R.layout.dialog_replace_rule, null); + bindView(view); + setContentView(view); + + } + + private ReplaceRuleDialog(Context context, ReplaceRuleBean replaceRuleBean, BookShelfBean bookShelfBean, int replaceUIMod) { + super(context, R.style.alertDialogTheme); + this.context = context; + this.replaceRuleBean = replaceRuleBean; + this.bookShelfBean = bookShelfBean; + this.ReplaceUIMode = replaceUIMod; + @SuppressLint("InflateParams") View view = LayoutInflater.from(context).inflate(R.layout.dialog_replace_rule, null); bindView(view); setContentView(view); @@ -52,12 +81,64 @@ private void bindView(View view) { tieUseTo = view.findViewById(R.id.tie_use_to); cbUseRegex = view.findViewById(R.id.cb_use_regex); tvOk = view.findViewById(R.id.tv_ok); + replace_ad_intro = view.findViewById(R.id.replace_ad_intro); + tvtitle = view.findViewById(R.id.title); + til_replace_to=view.findViewById(R.id.til_replace_to); if (replaceRuleBean != null) { tieReplaceSummary.setText(replaceRuleBean.getReplaceSummary()); tieReplaceTo.setText(replaceRuleBean.getReplacement()); tieReplaceRule.setText(replaceRuleBean.getRegex()); tieUseTo.setText(replaceRuleBean.getUseTo()); cbUseRegex.setChecked(replaceRuleBean.getIsRegex()); + + // 初始化广告话术规则的UI + if (ReplaceUIMode == DefaultUI) { + if (replaceRuleBean.getReplaceSummary().matches("^" + view.getContext().getString(R.string.replace_ad) + ".*")) + ReplaceUIMode = AdUI; + } + if (ReplaceUIMode > DefaultUI) { +// tieReplaceTo.setVisibility(View.GONE); + til_replace_to.setVisibility(View.GONE); + cbUseRegex.setVisibility(View.GONE); + replace_ad_intro.setVisibility(View.VISIBLE); + tieReplaceSummary.setInputType(EditorInfo.TYPE_NULL); + tieReplaceRule.setMaxLines(8); + + if (ReplaceUIMode == AdUI) { + tvtitle.setText(view.getContext().getString(R.string.replace_ad_title)); + } else { + tvtitle.setText(view.getContext().getString(R.string.replace_add_ad_title)); + } + str_summary = view.getContext().getString(R.string.replace_ad); + TextWatcher mTextWatcher = new TextWatcher() { + private CharSequence temp; + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + temp = s; + String str=s.toString().trim(); + if (str.replaceAll("[\\s,]", "").length() > 0) + tieReplaceSummary.setText(str_summary + "-" + str); + else + tieReplaceSummary.setText(str_summary); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void afterTextChanged(Editable s) { +/* if (s.toString().replaceAll("[\\s,]", "").length() > 0) + tieReplaceSummary.setText(str_summary + "-" + temp); + else + tieReplaceSummary.setText(str_summary);*/ + } + }; + tieUseTo.addTextChangedListener(mTextWatcher); + + } } else { replaceRuleBean = new ReplaceRuleBean(); replaceRuleBean.setEnable(true); @@ -66,8 +147,10 @@ private void bindView(View view) { tieUseTo.setText(String.format("%s,%s", bookShelfBean.getBookInfoBean().getName(), bookShelfBean.getTag())); } } + } + public ReplaceRuleDialog setPositiveButton(Callback callback) { tvOk.setOnClickListener(v -> { replaceRuleBean.setReplaceSummary(getEditableText(tieReplaceSummary.getText())); @@ -92,4 +175,5 @@ public interface Callback { void onPositiveButton(ReplaceRuleBean replaceRuleBean); } + } diff --git a/app/src/main/java/com/kunfei/bookshelf/widget/page/ChapterProvider.java b/app/src/main/java/com/kunfei/bookshelf/widget/page/ChapterProvider.java index a5332fe21e..d692aaecbc 100644 --- a/app/src/main/java/com/kunfei/bookshelf/widget/page/ChapterProvider.java +++ b/app/src/main/java/com/kunfei/bookshelf/widget/page/ChapterProvider.java @@ -69,12 +69,17 @@ private TxtChapter loadPageList(BookChapterBean chapter, @NonNull String content txtChapter.addPage(page); return txtChapter; } + Log.i("content-1",chapter.getDurChapterName()+"\n"+content.substring(content.length()/3*2)); content = contentHelper.replaceContent(pageLoader.book.getBookInfoBean().getName(), pageLoader.book.getTag(), content, pageLoader.book.getReplaceEnable()); -// Log.i("content",content); + // Log.i("chapterName",chapter.getDurChapterName()); // 方便debug -// if(chapter.getDurChapterName().matches(".*宣战.*")) +// if(chapter.getDurChapterName().matches(".*幽魂.*")) + { +// Log.i("content",content); + content = contentHelper.LightNovelParagraph2(content,chapter.getDurChapterName()); + } String[] allLine = content.split("\n"); List lines = new ArrayList<>(); List txtLists = new ArrayList<>();//记录每个字的位置 //pzl diff --git a/app/src/main/java/com/kunfei/bookshelf/widget/page/PageView.java b/app/src/main/java/com/kunfei/bookshelf/widget/page/PageView.java index 9ba539afbb..42b62c9750 100644 --- a/app/src/main/java/com/kunfei/bookshelf/widget/page/PageView.java +++ b/app/src/main/java/com/kunfei/bookshelf/widget/page/PageView.java @@ -436,8 +436,8 @@ private void drawOaleSeletLinesBg(Canvas canvas) {// 绘制椭圆型的选中背 RectF rect = new RectF(fistchar.getTopLeftPosition().x, fistchar.getTopLeftPosition().y, lastchar.getTopRightPosition().x, lastchar.getBottomRightPosition().y); - canvas.drawRoundRect(rect, fw / 2, - textHeight / 2, mTextSelectPaint); + canvas.drawRoundRect(rect, fw / 4, + textHeight /4, mTextSelectPaint); } } } diff --git a/app/src/main/res/drawable/ic_baseline_label.xml b/app/src/main/res/drawable/ic_baseline_label.xml new file mode 100644 index 0000000000..f23f1b6935 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_label.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_book_read.xml b/app/src/main/res/layout/activity_book_read.xml index b9b2824fff..c0acbf7e43 100644 --- a/app/src/main/res/layout/activity_book_read.xml +++ b/app/src/main/res/layout/activity_book_read.xml @@ -50,7 +50,7 @@ diff --git a/app/src/main/res/layout/dialog_replace_rule.xml b/app/src/main/res/layout/dialog_replace_rule.xml index baf8cf4fdd..86602465be 100644 --- a/app/src/main/res/layout/dialog_replace_rule.xml +++ b/app/src/main/res/layout/dialog_replace_rule.xml @@ -12,6 +12,7 @@ android:padding="10dp"> + + + android:focusable="true" + android:orientation="vertical"> @@ -42,24 +41,42 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_book_source_activity.xml b/app/src/main/res/menu/menu_book_source_activity.xml index de5b4ce4b5..12b0b276ef 100644 --- a/app/src/main/res/menu/menu_book_source_activity.xml +++ b/app/src/main/res/menu/menu_book_source_activity.xml @@ -95,6 +95,11 @@ android:icon="@drawable/ic_check_source" android:title="@string/check_select_source" app:showAsAction="never" /> + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7715bef05a..dcba965512 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -478,4 +478,10 @@ 自动重分段落 分享书籍 扫二维码 + 复制网址 + 标记所选发现源 + 广告话术 + 编辑广告话术规则 + 添加广告话术规则 + 广告话术功能使用了自动优化的算法,不需要使用正则表达式,只需要多次标记不想看的文字,就可以获得较好的广告替换效果。同一个网站的广告话术自动保存在一个规则内,避免了“替换净化”列表里有太多多的规则。