Merge pull request #25 from gedoor/master

up
pull/379/head
口口吕 5 years ago committed by GitHub
commit aee154f64c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/src/main/AndroidManifest.xml
  2. 9
      app/src/main/assets/updateLog.md
  3. 53
      app/src/main/assets/web/bookshelf.html
  4. 70
      app/src/main/assets/web/bookshelf.js
  5. 36
      app/src/main/java/io/legado/app/App.kt
  6. 22
      app/src/main/java/io/legado/app/constant/AppConst.kt
  7. 3
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  8. 27
      app/src/main/java/io/legado/app/help/ActivityHelp.kt
  9. 19
      app/src/main/java/io/legado/app/help/AppConfig.kt
  10. 4
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  11. 7
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  12. 10
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  13. 76
      app/src/main/java/io/legado/app/model/WebBook.kt
  14. 37
      app/src/main/java/io/legado/app/service/DownloadService.kt
  15. 16
      app/src/main/java/io/legado/app/ui/about/AboutActivity.kt
  16. 18
      app/src/main/java/io/legado/app/ui/about/DonateFragment.kt
  17. 6
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt
  18. 2
      app/src/main/java/io/legado/app/ui/book/download/DownloadActivity.kt
  19. 38
      app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt
  20. 2
      app/src/main/java/io/legado/app/ui/book/local/ImportBookAdapter.kt
  21. 2
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  22. 11
      app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt
  23. 2
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  24. 6
      app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt
  25. 48
      app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt
  26. 17
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  27. 9
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  28. 7
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  29. 24
      app/src/main/java/io/legado/app/ui/filechooser/FilePicker.kt
  30. 29
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  31. 7
      app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt
  32. 103
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  33. 43
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt
  34. 7
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  35. 67
      app/src/main/java/io/legado/app/ui/widget/BatteryView.kt
  36. 9
      app/src/main/java/io/legado/app/ui/widget/font/FontAdapter.kt
  37. 12
      app/src/main/java/io/legado/app/utils/ViewExtensions.kt
  38. 8
      app/src/main/java/io/legado/app/web/HttpServer.kt
  39. 40
      app/src/main/java/io/legado/app/web/controller/BookshelfController.kt
  40. 6
      app/src/main/java/io/legado/app/web/controller/SourceController.kt
  41. 27
      app/src/main/res/layout/activity_rss_read.xml
  42. 30
      app/src/main/res/layout/dialog_read_padding.xml
  43. 2
      app/src/main/res/layout/fragment_books.xml
  44. 17
      app/src/main/res/layout/view_book_page.xml
  45. 2
      app/src/main/res/layout/view_read_menu.xml
  46. 4
      app/src/main/res/menu/arrange_book.xml
  47. 7
      app/src/main/res/menu/book_group_manage.xml
  48. 2
      app/src/main/res/values/strings.xml
  49. 6
      app/src/main/res/xml/donate.xml

@ -186,6 +186,7 @@
<activity <activity
android:name="io.legado.app.ui.book.chapterlist.ChapterListActivity" android:name="io.legado.app.ui.book.chapterlist.ChapterListActivity"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--RSS阅读-->
<activity <activity
android:name=".ui.rss.read.ReadRssActivity" android:name=".ui.rss.read.ReadRssActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"

@ -2,6 +2,15 @@
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
* 请关注[开源阅读软件]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。 * 请关注[开源阅读软件]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
**2020/03/08**
* 订阅长按保存图片
* 订阅全屏播放
* 书架全部分组可以隐藏了
* 内置web书架基本能用了 by 六月
* 书架整理加入未分组
* 显示总进度
* 隐藏状态栏时,标题显示在上方
**2020/03/07** **2020/03/07**
* 添加标题上下间距调整 * 添加标题上下间距调整
* 添加标题大小调整 * 添加标题大小调整

@ -1,32 +1,39 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8" />
<title>阅读书架</title> <title>阅读3.0书架</title>
<link href="bookshelf.css" rel="stylesheet"/> <link rel="icon" href="favicon.ico">
<link href="bookshelf.css" rel="stylesheet" />
</head> </head>
<body> <body>
<button id="top" class="top"></button> <button id="top" class="top"></button>
<button id="showchapter" class="showchapter"></button> <button id="showchapter" class="showchapter"></button>
<button id="hidebooks" class="hidebooks"></button> <button id="hidebooks" class="hidebooks"></button>
<div class="nav"> <div class="nav">
<button id="back">返回</button> <button id="back">返回</button>
<button id="type">所有书籍 ▼</button> <button id="type">所有书籍 ▼</button>
<button id="sort">手动排序 ▼</button> <button id="sort">手动排序 ▼</button>
<button id="setting">阅读设置</button> <button id="setting">阅读设置</button>
<input type="text" class="address" id="address" title="阅读APP地址或IP" value=""/> <input type="text" class="address" id="address" title="阅读APP地址或IP" value="" />
<button id="refresh">重新加载</button> <button id="refresh">重新加载</button>
</div> </div>
<div class="allcontent" id="allcontent"> <div class="allcontent" id="allcontent">
<div id="books" class="books"></div> <div id="books" class="books"></div>
<div id="more" class="more"> <div id="more" class="more">
<div id="info" class="info"></div> <div id="info" class="info"></div>
<div class="clear"></div> <div class="clear"></div>
<div id="chapter" class="chapter"></div> <div id="chapter" class="chapter"></div>
<div id="content" class="content"></div> <div id="content" class="content"></div>
<div id="page" class="button">
<center><button id='up'>上一章</button><button id='down'>下一章</button></center>
</div>
</div>
</div> </div>
</div> <script src="bookshelf.js"></script>
<script src="bookshelf.js"></script>
</body> </body>
</html> </html>

@ -1,11 +1,14 @@
var $ = document.querySelector.bind(document) var $ = document.querySelector.bind(document)
, $$ = document.querySelectorAll.bind(document) , $$ = document.querySelectorAll.bind(document)
, $c = document.createElement.bind(document) , $c = document.createElement.bind(document)
, randomImg = "http://acg.bakayun.cn/randbg.php?t=dfzh" , randomImg = "http://api.mtyqx.cn/api/random.php"
, randomImg2 = "http://img.xjh.me/random_img.php" , randomImg2 = "http://img.xjh.me/random_img.php"
, books , books
; ;
var now_chapter = -1;
var sum_chapter = 0;
var formatTime = value => { var formatTime = value => {
return new Date(value).toLocaleString('zh-CN', { return new Date(value).toLocaleString('zh-CN', {
hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"
@ -13,13 +16,13 @@ var formatTime = value => {
}; };
var apiMap = { var apiMap = {
getBookshelf: "/getBookshelf", "getBookshelf": "/getBookshelf",
getChapterList: "/getChapterList", "getChapterList": "/getChapterList",
getBookContent: "/getBookContent", "getBookContent": "/getBookContent",
saveBook: "/saveBook" "saveBook": "/saveBook"
}; };
var apiAddress = (apiName, url) => { var apiAddress = (apiName, url, index) => {
let address = $('#address').value || window.location.host; let address = $('#address').value || window.location.host;
if (!(/^http|^\/\//).test(address)) { if (!(/^http|^\/\//).test(address)) {
address = "//" + address; address = "//" + address;
@ -28,6 +31,9 @@ var apiAddress = (apiName, url) => {
address += ":1122"; address += ":1122";
} }
localStorage.setItem('address', address); localStorage.setItem('address', address);
if (apiName == "getBookContent") {
return address + apiMap[apiName] + (url ? "?url=" + encodeURIComponent(url) : "") + "&index=" + index;
}
return address + apiMap[apiName] + (url ? "?url=" + encodeURIComponent(url) : ""); return address + apiMap[apiName] + (url ? "?url=" + encodeURIComponent(url) : "");
}; };
@ -41,12 +47,12 @@ var init = () => {
alert(getBookshelf.errorMsg); alert(getBookshelf.errorMsg);
return; return;
} }
books = data.data.sort((book1, book2) => book1.serialNumber - book2.serialNumber); books = data.data;
books.forEach(book => { books.forEach((book, i) => {
let bookDiv = $c("div"); let bookDiv = $c("div");
let img = $c("img"); let img = $c("img");
img.src = book.coverUrl || randomImg; img.src = book.coverUrl || randomImg;
img.setAttribute("data-series-num", book.serialNumber); img.setAttribute("data-series-num", i);
bookDiv.appendChild(img); bookDiv.appendChild(img);
bookDiv.innerHTML += `<table><tbody> bookDiv.innerHTML += `<table><tbody>
<tr><td>书名</td><td>${book.name}</td></tr> <tr><td>书名</td><td>${book.name}</td></tr>
@ -59,6 +65,8 @@ var init = () => {
}); });
$$('#books img').forEach(bookImg => $$('#books img').forEach(bookImg =>
bookImg.addEventListener("click", () => { bookImg.addEventListener("click", () => {
now_chapter = -1;
sum_chapter = 0;
$('#allcontent').classList.add("read"); $('#allcontent').classList.add("read");
var book = books[bookImg.getAttribute("data-series-num")]; var book = books[bookImg.getAttribute("data-series-num")];
$("#info").innerHTML = `<img src="${bookImg.src}"> $("#info").innerHTML = `<img src="${bookImg.src}">
@ -85,11 +93,13 @@ var init = () => {
data.data.forEach(chapter => { data.data.forEach(chapter => {
let ch = $c("button"); let ch = $c("button");
ch.setAttribute("data-url", chapter.durChapterUrl); ch.setAttribute("data-url", chapter.bookUrl);
ch.setAttribute("title", chapter.durChapterName); ch.setAttribute("data-index", chapter.index);
ch.innerHTML = chapter.durChapterName.length > 15 ? chapter.durChapterName.substring(0, 14) + "..." : chapter.durChapterName; ch.setAttribute("title", chapter.title);
ch.innerHTML = chapter.title.length > 15 ? chapter.title.substring(0, 14) + "..." : chapter.title;
$("#chapter").appendChild(ch); $("#chapter").appendChild(ch);
}); });
sum_chapter = data.data.length;
$('#chapter').scrollTop = 0; $('#chapter').scrollTop = 0;
$("#content").innerHTML = "章节列表加载完成!"; $("#content").innerHTML = "章节列表加载完成!";
}); });
@ -126,15 +136,47 @@ $('#showchapter').addEventListener("click", () => {
window.location.hash = "#chapter"; window.location.hash = "#chapter";
}); });
$('#up').addEventListener('click', e => {
if (now_chapter > 0) {
now_chapter--;
let clickEvent = document.createEvent('MouseEvents');
clickEvent.initEvent("click", true, false);
$('[data-index="' + now_chapter + '"]').dispatchEvent(clickEvent);
} else if (now_chapter == 0) {
alert("已经是第一章了^_^!")
} else {
}
});
$('#down').addEventListener('click', e => {
if (now_chapter > -1) {
if (now_chapter < sum_chapter - 1) {
now_chapter++;
let clickEvent = document.createEvent('MouseEvents');
clickEvent.initEvent("click", true, false);
$('[data-index="' + now_chapter + '"]').dispatchEvent(clickEvent);
} else {
alert("已经是最后一章了^_^!")
}
}
});
$('#chapter').addEventListener("click", (e) => { $('#chapter').addEventListener("click", (e) => {
if (e.target.tagName === "BUTTON") { if (e.target.tagName === "BUTTON") {
var url = e.target.getAttribute("data-url"); var url = e.target.getAttribute("data-url");
var index = e.target.getAttribute("data-index");
var name = e.target.getAttribute("title"); var name = e.target.getAttribute("title");
if (!url) { if (!url) {
alert("未取得章节地址"); alert("未取得书籍地址");
}
if (!index && (0 != index)) {
alert("未取得章节索引");
} }
now_chapter = parseInt(index);
$("#content").innerHTML = "<p>" + name + " 加载中...</p>"; $("#content").innerHTML = "<p>" + name + " 加载中...</p>";
fetch(apiAddress("getBookContent", url), { mode: "cors" }) fetch(apiAddress("getBookContent", url, index), { mode: "cors" })
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if (!data.isSuccess) { if (!data.isSuccess) {

@ -1,13 +1,11 @@
package io.legado.app package io.legado.app
import android.app.Activity
import android.app.Application import android.app.Application
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import com.jeremyliao.liveeventbus.LiveEventBus import com.jeremyliao.liveeventbus.LiveEventBus
@ -56,7 +54,7 @@ class App : Application() {
.lifecycleObserverAlwaysActive(true) .lifecycleObserverAlwaysActive(true)
.autoClear(false) .autoClear(false)
registerActivityLife() registerActivityLifecycleCallbacks(ActivityHelp)
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
@ -116,9 +114,7 @@ class App : Application() {
*/ */
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun createChannelId() { private fun createChannelId() {
val notificationManager = (getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager)?.let {
getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
notificationManager?.let {
//用唯一的ID创建渠道对象 //用唯一的ID创建渠道对象
val downloadChannel = NotificationChannel( val downloadChannel = NotificationChannel(
channelIdDownload, channelIdDownload,
@ -157,32 +153,4 @@ class App : Application() {
} }
} }
private fun registerActivityLife() {
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityDestroyed(activity: Activity) {
ActivityHelp.remove(activity)
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
ActivityHelp.add(activity)
}
})
}
} }

@ -4,8 +4,6 @@ import android.annotation.SuppressLint
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.putPrefBoolean
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import javax.script.ScriptEngine import javax.script.ScriptEngine
import javax.script.ScriptEngineManager import javax.script.ScriptEngineManager
@ -29,14 +27,18 @@ object AppConst {
ScriptEngineManager().getEngineByName("rhino") ScriptEngineManager().getEngineByName("rhino")
} }
val TIME_FORMAT: SimpleDateFormat by lazy { val timeFormat: SimpleDateFormat by lazy {
SimpleDateFormat("HH:mm") SimpleDateFormat("HH:mm")
} }
val DATE_FORMAT: SimpleDateFormat by lazy { val dateFormat: SimpleDateFormat by lazy {
SimpleDateFormat("yyyy/MM/dd HH:mm") SimpleDateFormat("yyyy/MM/dd HH:mm")
} }
val fileNameFormat: SimpleDateFormat by lazy {
SimpleDateFormat("yy-MM-dd-HH-mm-ss")
}
val keyboardToolChars: List<String> by lazy { val keyboardToolChars: List<String> by lazy {
arrayListOf( arrayListOf(
"@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", "$", "#", "!", ".", "@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", "$", "#", "!", ".",
@ -48,18 +50,6 @@ object AppConst {
val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local)) val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local))
val bookGroupAudio = BookGroup(-3, App.INSTANCE.getString(R.string.audio)) val bookGroupAudio = BookGroup(-3, App.INSTANCE.getString(R.string.audio))
var bookGroupLocalShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupLocal", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupLocal", value)
}
var bookGroupAudioShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupAudio", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupAudio", value)
}
const val notificationIdRead = 1144771 const val notificationIdRead = 1144771
const val notificationIdAudio = 1144772 const val notificationIdAudio = 1144772
const val notificationIdWeb = 1144773 const val notificationIdWeb = 1144773

@ -27,6 +27,9 @@ interface BookDao {
@Query("SELECT * FROM books WHERE (`group` & :group) > 0") @Query("SELECT * FROM books WHERE (`group` & :group) > 0")
fun observeByGroup(group: Int): LiveData<List<Book>> fun observeByGroup(group: Int): LiveData<List<Book>>
@Query("select * from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0")
fun observeNoGroup(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'") @Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
fun liveDataSearch(key: String): LiveData<List<Book>> fun liveDataSearch(key: String): LiveData<List<Book>>

@ -1,13 +1,15 @@
package io.legado.app.help package io.legado.app.help
import android.app.Activity import android.app.Activity
import android.app.Application
import android.os.Bundle
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*
/** /**
* Activity管理器,管理项目中Activity的状态 * Activity管理器,管理项目中Activity的状态
*/ */
object ActivityHelp { object ActivityHelp : Application.ActivityLifecycleCallbacks {
private val activities: MutableList<WeakReference<Activity>> = arrayListOf() private val activities: MutableList<WeakReference<Activity>> = arrayListOf()
@ -86,4 +88,27 @@ object ActivityHelp {
} }
} }
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityDestroyed(activity: Activity) {
remove(activity)
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
add(activity)
}
} }

@ -92,4 +92,23 @@ object AppConfig {
set(value) { set(value) {
App.INSTANCE.putPrefInt(PreferKey.systemTypefaces, value) App.INSTANCE.putPrefInt(PreferKey.systemTypefaces, value)
} }
var bookGroupAllShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupAll", true)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupAll", value)
}
var bookGroupLocalShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupLocal", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupLocal", value)
}
var bookGroupAudioShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupAudio", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupAudio", value)
}
} }

@ -2,7 +2,7 @@ package io.legado.app.help
import android.util.Base64 import android.util.Base64
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.constant.AppConst.DATE_FORMAT import io.legado.app.constant.AppConst.dateFormat
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.EncoderUtils import io.legado.app.utils.EncoderUtils
import io.legado.app.utils.MD5Utils import io.legado.app.utils.MD5Utils
@ -50,6 +50,6 @@ interface JsExtensions {
} }
fun timeFormat(time: Long): String { fun timeFormat(time: Long): String {
return DATE_FORMAT.format(Date(time)) return dateFormat.format(Date(time))
} }
} }

@ -251,6 +251,12 @@ object ReadBookConfig {
if (shareLayout) shareConfig.footerPaddingTop = value if (shareLayout) shareConfig.footerPaddingTop = value
else durConfig.footerPaddingTop = value else durConfig.footerPaddingTop = value
var showHeaderLine: Boolean
get() = if (shareLayout) shareConfig.showHeaderLine else durConfig.showHeaderLine
set(value) =
if (shareLayout) shareConfig.showHeaderLine = value
else durConfig.showHeaderLine = value
var showFooterLine: Boolean var showFooterLine: Boolean
get() = if (shareLayout) shareConfig.showFooterLine else durConfig.showFooterLine get() = if (shareLayout) shareConfig.showFooterLine else durConfig.showFooterLine
set(value) = set(value) =
@ -288,6 +294,7 @@ object ReadBookConfig {
var footerPaddingLeft: Int = 16, var footerPaddingLeft: Int = 16,
var footerPaddingRight: Int = 16, var footerPaddingRight: Int = 16,
var footerPaddingTop: Int = 6, var footerPaddingTop: Int = 6,
var showHeaderLine: Boolean = false,
var showFooterLine: Boolean = true var showFooterLine: Boolean = true
) { ) {
fun setBg(bgType: Int, bg: String) { fun setBg(bgType: Int, bg: String) {

@ -57,6 +57,16 @@ object HttpHelper {
return null return null
} }
suspend fun simpleGetByteAsync(url: String): ByteArray? {
NetworkUtils.getBaseUrl(url)?.let { baseUrl ->
return getByteRetrofit(baseUrl)
.create(HttpGetApi::class.java)
.getMapByteAsync(url, mapOf(), mapOf())
.body()
}
return null
}
inline fun <reified T> getApiService(baseUrl: String, encode: String? = null): T { inline fun <reified T> getApiService(baseUrl: String, encode: String? = null): T {
return getRetrofit(baseUrl, encode).create(T::class.java) return getRetrofit(baseUrl, encode).create(T::class.java)
} }

@ -84,8 +84,8 @@ class WebBook(val bookSource: BookSource) {
scope: CoroutineScope = Coroutine.DEFAULT, scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<Book> { ): Coroutine<Book> {
book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
book.type = bookSource.bookSourceType
val body = val body =
if (!book.infoHtml.isNullOrEmpty()) { if (!book.infoHtml.isNullOrEmpty()) {
book.infoHtml book.infoHtml
@ -111,8 +111,8 @@ class WebBook(val bookSource: BookSource) {
scope: CoroutineScope = Coroutine.DEFAULT, scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<List<BookChapter>> { ): Coroutine<List<BookChapter>> {
book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
book.type = bookSource.bookSourceType
val body = val body =
if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) { if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {
book.tocHtml book.tocHtml
@ -139,36 +139,50 @@ class WebBook(val bookSource: BookSource) {
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<String> { ): Coroutine<String> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
if (bookSource.getContentRule().content.isNullOrEmpty()) { getContentSuspend(
Debug.log(sourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}") book, bookChapter, nextChapterUrl, scope
return@async bookChapter.url
}
val body =
if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) {
book.tocHtml
} else {
val analyzeUrl =
AnalyzeUrl(
book = book,
ruleUrl = bookChapter.url,
baseUrl = book.tocUrl,
headerMapF = bookSource.getHeaderMap()
)
analyzeUrl.getResponseAwait(
bookSource.bookSourceUrl,
jsStr = bookSource.getContentRule().webJs,
sourceRegex = bookSource.getContentRule().sourceRegex
).body
}
BookContent.analyzeContent(
this,
body,
book,
bookChapter,
bookSource,
bookChapter.url,
nextChapterUrl
) )
} }
} }
/**
* 章节内容
*/
suspend fun getContentSuspend(
book: Book,
bookChapter: BookChapter,
nextChapterUrl: String? = null,
scope: CoroutineScope = Coroutine.DEFAULT
): String {
if (bookSource.getContentRule().content.isNullOrEmpty()) {
Debug.log(sourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}")
return bookChapter.url
}
val body =
if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) {
book.tocHtml
} else {
val analyzeUrl =
AnalyzeUrl(
book = book,
ruleUrl = bookChapter.url,
baseUrl = book.tocUrl,
headerMapF = bookSource.getHeaderMap()
)
analyzeUrl.getResponseAwait(
bookSource.bookSourceUrl,
jsStr = bookSource.getContentRule().webJs,
sourceRegex = bookSource.getContentRule().sourceRegex
).body
}
return BookContent.analyzeContent(
scope,
body,
book,
bookChapter,
bookSource,
bookChapter.url,
nextChapterUrl
)
}
} }

@ -29,8 +29,10 @@ class DownloadService : BaseService() {
private val handler = Handler() private val handler = Handler()
private var runnable: Runnable = Runnable { upDownload() } private var runnable: Runnable = Runnable { upDownload() }
private val downloadMap = hashMapOf<String, LinkedHashSet<BookChapter>>() private val downloadMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private val downloadCount = hashMapOf<String, DownloadCount>();
private val finalMap = hashMapOf<String, LinkedHashSet<BookChapter>>() private val finalMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private var notificationContent = "正在启动下载" private var notificationContent = "正在启动下载"
private val notificationBuilder by lazy { private val notificationBuilder by lazy {
val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload) val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload)
.setSmallIcon(R.drawable.ic_download) .setSmallIcon(R.drawable.ic_download)
@ -97,6 +99,11 @@ class DownloadService : BaseService() {
finalMap.remove(bookUrl) finalMap.remove(bookUrl)
} }
private fun updateNotification(downloadCount:DownloadCount, totalCount: Int, content: String){
notificationContent =
"进度:${downloadCount.downloadFinishedCount}/$totalCount,成功:${downloadCount.successCount},$content"
}
private fun download() { private fun download() {
val task = Coroutine.async(this, context = searchPool) { val task = Coroutine.async(this, context = searchPool) {
downloadMap.forEach { entry -> downloadMap.forEach { entry ->
@ -106,6 +113,9 @@ class DownloadService : BaseService() {
val bookSource = val bookSource =
App.db.bookSourceDao().getBookSource(book.origin) ?: return@async App.db.bookSourceDao().getBookSource(book.origin) ?: return@async
val webBook = WebBook(bookSource) val webBook = WebBook(bookSource)
downloadCount[entry.key] = DownloadCount()
entry.value.forEach { chapter -> entry.value.forEach { chapter ->
if (!isActive) return@async if (!isActive) return@async
if (downloadMap.containsKey(book.bookUrl)) { if (downloadMap.containsKey(book.bookUrl)) {
@ -116,16 +126,19 @@ class DownloadService : BaseService() {
scope = this, scope = this,
context = searchPool context = searchPool
) )
.onStart { //.onStart {
notificationContent = chapter.title // notificationContent = "启动:" + chapter.title
} //}
.onSuccess(IO) { content -> .onSuccess(IO) { content ->
content?.let { content?.let {
downloadCount[entry.key]?.increaseSuccess()
BookHelp.saveContent(book, chapter, content) BookHelp.saveContent(book, chapter, content)
} }
} }
.onFinally(IO) { .onFinally(IO) {
synchronized(this@DownloadService) { synchronized(this@DownloadService) {
downloadCount[entry.key]?.increaseFinished()
downloadCount[entry.key]?.let { updateNotification(it, entry.value.size, chapter.title) }
val chapterMap = val chapterMap =
finalMap[book.bookUrl] finalMap[book.bookUrl]
?: linkedSetOf<BookChapter>().apply { ?: linkedSetOf<BookChapter>().apply {
@ -135,9 +148,14 @@ class DownloadService : BaseService() {
if (chapterMap.size == entry.value.size) { if (chapterMap.size == entry.value.size) {
downloadMap.remove(book.bookUrl) downloadMap.remove(book.bookUrl)
finalMap.remove(book.bookUrl) finalMap.remove(book.bookUrl)
downloadCount.remove(entry.key)
} }
} }
} }
} else{
//无需下载的,设置为增加成功
downloadCount[entry.key]?.increaseSuccess()
downloadCount[entry.key]?.increaseFinished()
} }
} }
} }
@ -177,3 +195,16 @@ class DownloadService : BaseService() {
startForeground(AppConst.notificationIdDownload, notification) startForeground(AppConst.notificationIdDownload, notification)
} }
} }
class DownloadCount{
@Volatile public var downloadFinishedCount = 0 // 下载完成的条目数量
@Volatile public var successCount = 0 //下载成功的条目数量
fun increaseSuccess(){
++successCount;
}
fun increaseFinished(){
++downloadFinishedCount;
}
}

@ -1,13 +1,19 @@
package io.legado.app.ui.about package io.legado.app.ui.about
import android.os.Bundle import android.os.Bundle
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity import io.legado.app.base.BaseActivity
import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.openUrl import io.legado.app.utils.openUrl
import kotlinx.android.synthetic.main.activity_about.*
import org.jetbrains.anko.share import org.jetbrains.anko.share
class AboutActivity : BaseActivity(R.layout.activity_about) { class AboutActivity : BaseActivity(R.layout.activity_about) {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -17,6 +23,16 @@ class AboutActivity : BaseActivity(R.layout.activity_about) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.fl_fragment, aboutFragment, fTag) .replace(R.id.fl_fragment, aboutFragment, fTag)
.commit() .commit()
tv_app_summary.post {
val span = ForegroundColorSpan(accentColor)
val spannableString = SpannableString(tv_app_summary.text)
val start = spannableString.indexOf("开源阅读软件")
spannableString.setSpan(
span, start, start + 6,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
tv_app_summary.text = spannableString
}
} }
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {

@ -2,7 +2,6 @@ package io.legado.app.ui.about
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.preference.Preference import androidx.preference.Preference
@ -12,7 +11,6 @@ import io.legado.app.utils.ACache
import io.legado.app.utils.openUrl import io.legado.app.utils.openUrl
import io.legado.app.utils.sendToClip import io.legado.app.utils.sendToClip
import org.jetbrains.anko.longToast import org.jetbrains.anko.longToast
import java.net.URLEncoder
class DonateFragment : PreferenceFragmentCompat() { class DonateFragment : PreferenceFragmentCompat() {
@ -36,7 +34,6 @@ class DonateFragment : PreferenceFragmentCompat() {
"zfbHbRwm" -> requireContext().openUrl(zfbHbRwmUrl) "zfbHbRwm" -> requireContext().openUrl(zfbHbRwmUrl)
"zfbSkRwm" -> requireContext().openUrl(zfbSkRwmUrl) "zfbSkRwm" -> requireContext().openUrl(zfbSkRwmUrl)
"qqSkRwm" -> requireContext().openUrl(qqSkRwmUrl) "qqSkRwm" -> requireContext().openUrl(qqSkRwmUrl)
"zfbSk" -> aliDonate(requireContext())
"zfbHbSsm" -> getZfbHb(requireContext()) "zfbHbSsm" -> getZfbHb(requireContext())
"gzGzh" -> requireContext().sendToClip("开源阅读软件") "gzGzh" -> requireContext().sendToClip("开源阅读软件")
} }
@ -59,19 +56,4 @@ class DonateFragment : PreferenceFragmentCompat() {
} }
} }
private fun aliDonate(context: Context) {
try {
val qrCode = URLEncoder.encode(
"https://qr.alipay.com/tsx06677nwdk3javroq4ef0?_s=Dweb-other",
"utf-8"
)
val aliPayQr =
"alipayqr://platformapi/startapp?saId=10000007&qrcode=$qrCode&_t=${System.currentTimeMillis()}"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(aliPayQr))
context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
}
}
} }

@ -100,6 +100,7 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
-1 -> App.db.bookDao().observeAll() -1 -> App.db.bookDao().observeAll()
-2 -> App.db.bookDao().observeLocal() -2 -> App.db.bookDao().observeLocal()
-3 -> App.db.bookDao().observeAudio() -3 -> App.db.bookDao().observeAudio()
-11 -> App.db.bookDao().observeNoGroup()
else -> App.db.bookDao().observeByGroup(groupId) else -> App.db.bookDao().observeByGroup(groupId)
} }
booksLiveData?.observe(this, Observer { booksLiveData?.observe(this, Observer {
@ -112,6 +113,11 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
when (item.itemId) { when (item.itemId) {
R.id.menu_group_manage -> GroupManageDialog() R.id.menu_group_manage -> GroupManageDialog()
.show(supportFragmentManager, "groupManage") .show(supportFragmentManager, "groupManage")
R.id.menu_no_group -> {
title_bar.subtitle = getString(R.string.no_group)
groupId = -11
initBookData()
}
R.id.menu_all -> { R.id.menu_all -> {
title_bar.subtitle = item.title title_bar.subtitle = item.title
groupId = -1 groupId = -1

@ -127,7 +127,7 @@ class DownloadActivity : VMBaseActivity<DownloadViewModel>(R.layout.activity_dow
FilePicker.selectFolder(this, exportRequestCode) { FilePicker.selectFolder(this, exportRequestCode) {
val path = ACache.get(this@DownloadActivity).getAsString(exportBookPathKey) val path = ACache.get(this@DownloadActivity).getAsString(exportBookPathKey)
if (path.isNullOrEmpty()) { if (path.isNullOrEmpty()) {
toast("没有默认路径") toast(R.string.no_default_path)
} else { } else {
startExport(path) startExport(path)
} }

@ -20,9 +20,9 @@ import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.AppConst
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.help.AppConfig
import io.legado.app.help.ItemTouchCallback import io.legado.app.help.ItemTouchCallback
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.customView
@ -42,7 +42,7 @@ import kotlin.collections.ArrayList
class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
private lateinit var viewModel: GroupViewModel private lateinit var viewModel: GroupViewModel
private lateinit var adapter: GroupAdapter private lateinit var adapter: GroupAdapter
private var callBack: CallBack? = null private val callBack: CallBack? get() = parentFragment as? CallBack
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -62,19 +62,12 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
callBack = parentFragment as? CallBack tool_bar.title = getString(R.string.group_manage)
initData() initData()
initMenu()
} }
private fun initData() { private fun initData() {
tool_bar.title = getString(R.string.group_manage)
tool_bar.inflateMenu(R.menu.book_group_manage)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.setOnMenuItemClickListener(this)
tool_bar.menu.findItem(R.id.menu_group_local)
.isChecked = AppConst.bookGroupLocalShow
tool_bar.menu.findItem(R.id.menu_group_audio)
.isChecked = AppConst.bookGroupAudioShow
adapter = GroupAdapter(requireContext()) adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(VerticalDivider(requireContext())) recycler_view.addItemDecoration(VerticalDivider(requireContext()))
@ -90,17 +83,36 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view)
} }
private fun initMenu() {
tool_bar.setOnMenuItemClickListener(this)
tool_bar.inflateMenu(R.menu.book_group_manage)
tool_bar.menu.let {
it.applyTint(requireContext(), Theme.getTheme())
it.findItem(R.id.menu_group_all)
.isChecked = AppConfig.bookGroupAllShow
it.findItem(R.id.menu_group_local)
.isChecked = AppConfig.bookGroupLocalShow
it.findItem(R.id.menu_group_audio)
.isChecked = AppConfig.bookGroupAudioShow
}
}
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
R.id.menu_add -> addGroup() R.id.menu_add -> addGroup()
R.id.menu_group_all -> {
item.isChecked = !item.isChecked
AppConfig.bookGroupAllShow = item.isChecked
callBack?.upGroup()
}
R.id.menu_group_local -> { R.id.menu_group_local -> {
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
AppConst.bookGroupLocalShow = item.isChecked AppConfig.bookGroupLocalShow = item.isChecked
callBack?.upGroup() callBack?.upGroup()
} }
R.id.menu_group_audio -> { R.id.menu_group_audio -> {
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
AppConst.bookGroupAudioShow = item.isChecked AppConfig.bookGroupAudioShow = item.isChecked
callBack?.upGroup() callBack?.upGroup()
} }
} }

@ -87,7 +87,7 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) :
ll_brief.visible() ll_brief.visible()
tv_tag.text = item.name.substringAfterLast(".") tv_tag.text = item.name.substringAfterLast(".")
tv_size.text = StringUtils.toSize(item.size) tv_size.text = StringUtils.toSize(item.size)
tv_date.text = AppConst.DATE_FORMAT.format(item.date) tv_date.text = AppConst.dateFormat.format(item.date)
cb_select.isChecked = selectedUris.contains(item.uri.toString()) cb_select.isChecked = selectedUris.contains(item.uri.toString())
} }
tv_name.text = item.name tv_name.text = item.name

@ -479,6 +479,8 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
if (!ReadBook.isLocalBook) { if (!ReadBook.isLocalBook) {
tv_chapter_url.text = it.url tv_chapter_url.text = it.url
tv_chapter_url.visible() tv_chapter_url.visible()
} else {
tv_chapter_url.gone()
} }
seek_read_page.max = it.pageSize().minus(1) seek_read_page.max = it.pageSize().minus(1)
seek_read_page.progress = ReadBook.durPageIndex seek_read_page.progress = ReadBook.durPageIndex

@ -76,7 +76,8 @@ class PaddingConfigDialog : DialogFragment() {
dsb_footer_padding_bottom.progress = footerPaddingBottom dsb_footer_padding_bottom.progress = footerPaddingBottom
dsb_footer_padding_left.progress = footerPaddingLeft dsb_footer_padding_left.progress = footerPaddingLeft
dsb_footer_padding_right.progress = footerPaddingRight dsb_footer_padding_right.progress = footerPaddingRight
cb_show_line.isChecked = showFooterLine cb_show_top_line.isChecked = showHeaderLine
cb_show_bottom_line.isChecked = showFooterLine
} }
private fun initView() = with(ReadBookConfig) { private fun initView() = with(ReadBookConfig) {
@ -131,7 +132,13 @@ class PaddingConfigDialog : DialogFragment() {
footerPaddingRight = it footerPaddingRight = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
cb_show_line.onCheckedChangeListener = { cb, isChecked -> cb_show_top_line.onCheckedChangeListener = { cb, isChecked ->
if (cb.isPressed) {
showHeaderLine = isChecked
postEvent(EventBus.UP_CONFIG, true)
}
}
cb_show_bottom_line.onCheckedChangeListener = { cb, isChecked ->
if (cb.isPressed) { if (cb.isPressed) {
showFooterLine = isChecked showFooterLine = isChecked
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)

@ -108,7 +108,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
textBold = !textBold textBold = !textBold
tv_text_bold.isSelected = textBold tv_text_bold.isSelected = textBold
} }
postEvent(EventBus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, true)
} }
tv_text_font.onClick { tv_text_font.onClick {
FontSelectDialog().show(childFragmentManager, "fontSelectDialog") FontSelectDialog().show(childFragmentManager, "fontSelectDialog")

@ -270,8 +270,8 @@ object ChapterProvider {
contentPaint.isAntiAlias = true contentPaint.isAntiAlias = true
contentPaint.color = ReadBookConfig.durConfig.textColor() contentPaint.color = ReadBookConfig.durConfig.textColor()
contentPaint.letterSpacing = ReadBookConfig.letterSpacing contentPaint.letterSpacing = ReadBookConfig.letterSpacing
val bold = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL val style = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL
contentPaint.typeface = Typeface.create(typeface, bold) contentPaint.typeface = Typeface.create(typeface, style)
contentPaint.textSize = ReadBookConfig.textSize.sp.toFloat() contentPaint.textSize = ReadBookConfig.textSize.sp.toFloat()
//间距 //间距
lineSpacingExtra = ReadBookConfig.lineSpacingExtra lineSpacingExtra = ReadBookConfig.lineSpacingExtra
@ -294,5 +294,5 @@ object ChapterProvider {
} }
val TextPaint.textHeight: Float val TextPaint.textHeight: Float
get() = this.fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading get() = fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading
} }

@ -7,7 +7,7 @@ import android.view.MotionEvent
import android.widget.FrameLayout import android.widget.FrameLayout
import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst.TIME_FORMAT import io.legado.app.constant.AppConst.timeFormat
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage
@ -18,6 +18,8 @@ import java.util.*
class ContentView(context: Context) : FrameLayout(context) { class ContentView(context: Context) : FrameLayout(context) {
private var battery = 100
init { init {
//设置背景防止切换背景时文字重叠 //设置背景防止切换背景时文字重叠
setBackgroundColor(context.getCompatColor(R.color.background)) setBackgroundColor(context.getCompatColor(R.color.background))
@ -26,8 +28,7 @@ class ContentView(context: Context) : FrameLayout(context) {
upStyle() upStyle()
upTime() upTime()
content_text_view.upView = { content_text_view.upView = {
tv_bottom_left.text = it.title setProgress(it)
setPageIndex(it.index, it.pageSize)
} }
} }
@ -60,6 +61,7 @@ class ContentView(context: Context) : FrameLayout(context) {
footerPaddingRight.dp, footerPaddingRight.dp,
footerPaddingBottom.dp footerPaddingBottom.dp
) )
vw_top_divider.visible(showHeaderLine)
vw_bottom_divider.visible(showFooterLine) vw_bottom_divider.visible(showFooterLine)
content_text_view.upVisibleRect() content_text_view.upVisibleRect()
durConfig.textColor().let { durConfig.textColor().let {
@ -67,6 +69,14 @@ class ContentView(context: Context) : FrameLayout(context) {
tv_top_right.setTextColor(it) tv_top_right.setTextColor(it)
tv_bottom_left.setTextColor(it) tv_bottom_left.setTextColor(it)
tv_bottom_right.setTextColor(it) tv_bottom_right.setTextColor(it)
battery_view.setColor(it)
}
if (hideStatusBar) {
tv_bottom_left.text = timeFormat.format(Date(System.currentTimeMillis()))
battery_view.visible()
battery_view.setBattery(battery)
} else {
battery_view.gone()
} }
} }
} }
@ -85,20 +95,20 @@ class ContentView(context: Context) : FrameLayout(context) {
} }
fun upTime() { fun upTime() {
tv_top_left.text = TIME_FORMAT.format(Date(System.currentTimeMillis())) if (ReadBookConfig.hideStatusBar) {
tv_bottom_right.text = timeFormat.format(Date(System.currentTimeMillis()))
}
} }
fun upBattery(battery: Int) { fun upBattery(battery: Int) {
tv_top_right.text = context.getString(R.string.battery_show, battery) this.battery = battery
if (ReadBookConfig.hideStatusBar) {
battery_view.setBattery(battery)
}
} }
fun setContent(textPage: TextPage) { fun setContent(textPage: TextPage) {
tv_bottom_left.text = when (AppConfig.chineseConverterType) { setProgress(textPage)
1 -> ZhConvertBootstrap.newInstance().toSimple(textPage.title)
2 -> ZhConvertBootstrap.newInstance().toTraditional(textPage.title)
else -> textPage.title
}
setPageIndex(textPage.index, textPage.pageSize)
content_text_view.resetPageOffset() content_text_view.resetPageOffset()
content_text_view.setContent(textPage) content_text_view.setContent(textPage)
} }
@ -108,9 +118,19 @@ class ContentView(context: Context) : FrameLayout(context) {
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun setPageIndex(pageIndex: Int?, pageSize: Int) { fun setProgress(textPage: TextPage) = textPage.apply {
pageIndex?.let { val title = when (AppConfig.chineseConverterType) {
tv_bottom_right.text = "${pageIndex.plus(1)}/${pageSize}" 1 -> ZhConvertBootstrap.newInstance().toSimple(textPage.title)
2 -> ZhConvertBootstrap.newInstance().toTraditional(textPage.title)
else -> textPage.title
}
if (ReadBookConfig.hideStatusBar) {
tv_top_left.text = title
tv_top_right.text = readProgress
tv_bottom_left.text = "${index.plus(1)}/$pageSize"
} else {
tv_bottom_left.text = title
tv_bottom_right.text = "${index.plus(1)}/$pageSize $readProgress"
} }
} }

@ -5,6 +5,7 @@ import android.text.StaticLayout
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.book.read.page.ChapterProvider import io.legado.app.ui.book.read.page.ChapterProvider
import java.text.DecimalFormat
data class TextPage( data class TextPage(
var index: Int = 0, var index: Int = 0,
@ -84,4 +85,20 @@ data class TextPage(
lineStart += textLine.text.length lineStart += textLine.text.length
} }
} }
val readProgress: String
get() {
val df = DecimalFormat("0.0%")
if (chapterSize == 0 || pageSize == 0 && chapterIndex == 0) {
return "0.0%"
} else if (pageSize == 0) {
return df.format((chapterIndex + 1.0f) / chapterSize.toDouble())
}
var percent =
df.format(chapterIndex * 1.0f / chapterSize + 1.0f / chapterSize * (index + 1) / pageSize.toDouble())
if (percent == "100.0%" && (chapterIndex + 1 != chapterSize || index + 1 != pageSize)) {
percent = "99.9%"
}
return percent
}
} }

@ -283,15 +283,10 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
*/ */
@Synchronized @Synchronized
private fun upSearchItems(items: List<SearchBook>, isMandatoryUpdate: Boolean) { private fun upSearchItems(items: List<SearchBook>, isMandatoryUpdate: Boolean) {
if (!isMandatoryUpdate && System.currentTimeMillis() - refreshTime < 500) { if (isMandatoryUpdate || System.currentTimeMillis() - refreshTime > 500) {
return refreshTime = System.currentTimeMillis()
}
refreshTime = System.currentTimeMillis()
try {
val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), items)) val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), items))
adapter.setItems(items, diffResult) adapter.setItems(items, diffResult)
} catch (e: Exception) {
e.printStackTrace()
} }
} }

@ -85,7 +85,12 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
R.id.menu_group_manage -> R.id.menu_group_manage ->
GroupManageDialog().show(supportFragmentManager, "groupManage") GroupManageDialog().show(supportFragmentManager, "groupManage")
R.id.menu_import_source_local -> FilePicker R.id.menu_import_source_local -> FilePicker
.selectFile(this, importRequestCode, "text/*", arrayOf("txt", "json")) .selectFile(
this,
importRequestCode,
type = "text/*",
allowExtensions = arrayOf("txt", "json")
)
R.id.menu_import_source_onLine -> showImportDialog() R.id.menu_import_source_onLine -> showImportDialog()
} }
if (item.groupId == R.id.source_group) { if (item.groupId == R.id.source_group) {

@ -14,8 +14,13 @@ import org.jetbrains.anko.toast
@Suppress("unused") @Suppress("unused")
object FilePicker { object FilePicker {
fun selectFolder(activity: AppCompatActivity, requestCode: Int, default: (() -> Unit)? = null) { fun selectFolder(
activity.alert(titleResource = R.string.select_folder) { activity: AppCompatActivity,
requestCode: Int,
title: String = activity.getString(R.string.select_folder),
default: (() -> Unit)? = null
) {
activity.alert(title = title) {
val selectList = val selectList =
activity.resources.getStringArray(R.array.select_folder).toMutableList() activity.resources.getStringArray(R.array.select_folder).toMutableList()
default ?: let { default ?: let {
@ -45,9 +50,14 @@ object FilePicker {
}.show() }.show()
} }
fun selectFolder(fragment: Fragment, requestCode: Int, default: (() -> Unit)? = null) { fun selectFolder(
fragment: Fragment,
requestCode: Int,
title: String = fragment.getString(R.string.select_folder),
default: (() -> Unit)? = null
) {
fragment.requireContext() fragment.requireContext()
.alert(titleResource = R.string.select_folder) { .alert(title = title) {
val selectList = val selectList =
fragment.resources.getStringArray(R.array.select_folder).toMutableList() fragment.resources.getStringArray(R.array.select_folder).toMutableList()
default ?: let { default ?: let {
@ -80,11 +90,12 @@ object FilePicker {
fun selectFile( fun selectFile(
activity: BaseActivity, activity: BaseActivity,
requestCode: Int, requestCode: Int,
title: String = activity.getString(R.string.select_file),
type: String, type: String,
allowExtensions: Array<String>?, allowExtensions: Array<String>?,
default: (() -> Unit)? = null default: (() -> Unit)? = null
) { ) {
activity.alert(titleResource = R.string.select_file) { activity.alert(title = title) {
val selectList = val selectList =
activity.resources.getStringArray(R.array.select_folder).toMutableList() activity.resources.getStringArray(R.array.select_folder).toMutableList()
default ?: let { default ?: let {
@ -119,12 +130,13 @@ object FilePicker {
fun selectFile( fun selectFile(
fragment: Fragment, fragment: Fragment,
requestCode: Int, requestCode: Int,
title: String = fragment.getString(R.string.select_file),
type: String, type: String,
allowExtensions: Array<String>, allowExtensions: Array<String>,
default: (() -> Unit)? = null default: (() -> Unit)? = null
) { ) {
fragment.requireContext() fragment.requireContext()
.alert(titleResource = R.string.select_file) { .alert(title = title) {
val selectList = val selectList =
fragment.resources.getStringArray(R.array.select_folder).toMutableList() fragment.resources.getStringArray(R.array.select_folder).toMutableList()
default ?: let { default ?: let {

@ -17,6 +17,7 @@ import io.legado.app.base.VMBaseFragment
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.help.AppConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.noButton
@ -108,11 +109,13 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
synchronized(this) { synchronized(this) {
tab_layout.removeOnTabSelectedListener(this) tab_layout.removeOnTabSelectedListener(this)
bookGroups.clear() bookGroups.clear()
bookGroups.add(AppConst.bookGroupAll) if (AppConfig.bookGroupAllShow) {
if (AppConst.bookGroupLocalShow) { bookGroups.add(AppConst.bookGroupAll)
}
if (AppConfig.bookGroupLocalShow) {
bookGroups.add(AppConst.bookGroupLocal) bookGroups.add(AppConst.bookGroupLocal)
} }
if (AppConst.bookGroupAudioShow) { if (AppConfig.bookGroupAudioShow) {
bookGroups.add(AppConst.bookGroupAudio) bookGroups.add(AppConst.bookGroupAudio)
} }
bookGroups.addAll(it) bookGroups.addAll(it)
@ -134,13 +137,17 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
override fun upGroup() { override fun upGroup() {
synchronized(this) { synchronized(this) {
bookGroups.remove(AppConst.bookGroupAll)
bookGroups.remove(AppConst.bookGroupLocal) bookGroups.remove(AppConst.bookGroupLocal)
bookGroups.remove(AppConst.bookGroupAudio) bookGroups.remove(AppConst.bookGroupAudio)
if (getPrefBoolean("bookGroupAudio", true)) { if (AppConfig.bookGroupAudioShow) {
bookGroups.add(1, AppConst.bookGroupAudio) bookGroups.add(0, AppConst.bookGroupAudio)
}
if (AppConfig.bookGroupLocalShow) {
bookGroups.add(0, AppConst.bookGroupLocal)
} }
if (getPrefBoolean("bookGroupLocal", true)) { if (AppConfig.bookGroupAllShow) {
bookGroups.add(1, AppConst.bookGroupLocal) bookGroups.add(0, AppConst.bookGroupAll)
} }
bookshelfAdapter.notifyDataSetChanged() bookshelfAdapter.notifyDataSetChanged()
} }
@ -196,13 +203,9 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
}.show().applyTint() }.show().applyTint()
} }
override fun onTabReselected(tab: TabLayout.Tab?) { override fun onTabReselected(tab: TabLayout.Tab?) = Unit
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
} override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
override fun onTabSelected(tab: TabLayout.Tab?) { override fun onTabSelected(tab: TabLayout.Tab?) {
tab?.position?.let { tab?.position?.let {

@ -180,7 +180,12 @@ class ReplaceRuleActivity : VMBaseActivity<ReplaceRuleViewModel>(R.layout.activi
R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelection()) R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelection())
R.id.menu_import_source_onLine -> showImportDialog() R.id.menu_import_source_onLine -> showImportDialog()
R.id.menu_import_source_local -> FilePicker R.id.menu_import_source_local -> FilePicker
.selectFile(this, importRequestCode, "text/*", arrayOf("txt", "json")) .selectFile(
this,
importRequestCode,
type = "text/*",
allowExtensions = arrayOf("txt", "json")
)
} }
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
} }

@ -1,37 +1,42 @@
package io.legado.app.ui.rss.read package io.legado.app.ui.rss.read
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.*
import android.view.Menu import android.webkit.*
import android.view.MenuItem import androidx.core.view.size
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.lib.theme.DrawableUtils import io.legado.app.lib.theme.DrawableUtils
import io.legado.app.lib.theme.primaryTextColor import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.utils.NetworkUtils import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.utils.getViewModel import io.legado.app.ui.filechooser.FilePicker
import io.legado.app.utils.openUrl import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_rss_read.* import kotlinx.android.synthetic.main.activity_rss_read.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.apache.commons.text.StringEscapeUtils import org.apache.commons.text.StringEscapeUtils
import org.jetbrains.anko.share import org.jetbrains.anko.share
import org.jetbrains.anko.toast
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.safety.Whitelist import org.jsoup.safety.Whitelist
class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_read), class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_read),
FileChooserDialog.CallBack,
ReadRssViewModel.CallBack { ReadRssViewModel.CallBack {
override val viewModel: ReadRssViewModel override val viewModel: ReadRssViewModel
get() = getViewModel(ReadRssViewModel::class.java) get() = getViewModel(ReadRssViewModel::class.java)
private val savePathRequestCode = 132
private val imagePathKey = ""
private var starMenuItem: MenuItem? = null private var starMenuItem: MenuItem? = null
private var ttsMenuItem: MenuItem? = null private var ttsMenuItem: MenuItem? = null
private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null
private var webPic: String? = null
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
viewModel.callBack = this viewModel.callBack = this
@ -41,6 +46,20 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
viewModel.initData(intent) viewModel.initData(intent)
} }
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
when (newConfig.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
}
Configuration.ORIENTATION_PORTRAIT -> {
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
}
}
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.rss_read, menu) menuInflater.inflate(R.menu.rss_read, menu)
return super.onCompatCreateOptionsMenu(menu) return super.onCompatCreateOptionsMenu(menu)
@ -65,6 +84,20 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
} }
private fun initWebView() { private fun initWebView() {
web_view.webChromeClient = object : WebChromeClient() {
override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
ll_view.invisible()
custom_web_view.addView(view)
customWebViewCallback = callback
}
override fun onHideCustomView() {
custom_web_view.removeAllViews()
ll_view.visible()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
web_view.webViewClient = object : WebViewClient() { web_view.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading( override fun shouldOverrideUrlLoading(
view: WebView?, view: WebView?,
@ -95,6 +128,30 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
domStorageEnabled = true domStorageEnabled = true
allowContentAccess = true allowContentAccess = true
} }
web_view.setOnLongClickListener {
val hitTestResult = web_view.hitTestResult
if (hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) {
hitTestResult.extra?.let {
webPic = it
saveImage()
return@setOnLongClickListener true
}
}
return@setOnLongClickListener false
}
}
private fun saveImage() {
FilePicker.selectFolder(this, savePathRequestCode, getString(R.string.save_image)) {
val path = ACache.get(this).getAsString(imagePathKey)
if (path.isNullOrEmpty()) {
toast(R.string.no_default_path)
} else {
viewModel.saveImage(webPic, path)
}
}
} }
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
@ -113,7 +170,6 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
url url
)//不想用baseUrl进else )//不想用baseUrl进else
} else { } else {
//webView.loadData(html, "text/html;charset=utf-8", "utf-8")//经测试可以解决中文乱码
web_view.loadDataWithBaseURL( web_view.loadDataWithBaseURL(
null, null,
html, html,
@ -175,7 +231,10 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
event?.let { event?.let {
when (keyCode) { when (keyCode) {
KeyEvent.KEYCODE_BACK -> if (event.isTracking && !event.isCanceled && web_view.canGoBack()) { KeyEvent.KEYCODE_BACK -> if (event.isTracking && !event.isCanceled && web_view.canGoBack()) {
if (web_view.copyBackForwardList().size > 1) { if (custom_web_view.size > 0) {
customWebViewCallback?.onCustomViewHidden()
return true
} else if (web_view.copyBackForwardList().size > 1) {
web_view.goBack() web_view.goBack()
return true return true
} }
@ -203,6 +262,24 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
} }
} }
override fun onFilePicked(requestCode: Int, currentPath: String) {
when (requestCode) {
savePathRequestCode -> {
ACache.get(this).put(imagePathKey, currentPath)
viewModel.saveImage(webPic, currentPath)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
savePathRequestCode -> data?.data?.let {
onFilePicked(requestCode, it.toString())
}
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
web_view.destroy() web_view.destroy()

@ -2,19 +2,31 @@ package io.legado.app.ui.rss.read
import android.app.Application import android.app.Application
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.speech.tts.TextToSpeech import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener import android.speech.tts.UtteranceProgressListener
import android.util.Base64
import android.webkit.URLUtil
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppConst
import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.help.http.HttpHelper
import io.legado.app.model.Rss import io.legado.app.model.Rss
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.DocumentUtils
import io.legado.app.utils.FileUtils
import io.legado.app.utils.isContentPath
import io.legado.app.utils.writeBytes
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
import java.util.* import java.util.*
class ReadRssViewModel(application: Application) : BaseViewModel(application), class ReadRssViewModel(application: Application) : BaseViewModel(application),
TextToSpeech.OnInitListener { TextToSpeech.OnInitListener {
var callBack: CallBack? = null var callBack: CallBack? = null
@ -85,6 +97,37 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
} }
} }
fun saveImage(webPic: String?, path: String) {
webPic ?: return
execute {
val fileName = "${AppConst.fileNameFormat.format(Date(System.currentTimeMillis()))}.jpg"
webData2bitmap(webPic)?.let { biteArray ->
if (path.isContentPath()) {
val uri = Uri.parse(path)
DocumentFile.fromTreeUri(context, uri)?.let { doc ->
DocumentUtils.createFileIfNotExist(doc, fileName)
?.writeBytes(context, biteArray)
}
} else {
val file = FileUtils.createFileIfNotExist(File(path), fileName)
file.writeBytes(biteArray)
}
} ?: throw Throwable("NULL")
}.onError {
toast("保存图片失败:${it.localizedMessage}")
}.onSuccess {
toast("保存成功")
}
}
private suspend fun webData2bitmap(data: String): ByteArray? {
return if (URLUtil.isValidUrl(data)) {
HttpHelper.simpleGetByteAsync(data)
} else {
Base64.decode(data.split(",").toTypedArray()[1], Base64.DEFAULT)
}
}
fun clHtml(content: String): String { fun clHtml(content: String): String {
return if (content.contains("<style>".toRegex())) { return if (content.contains("<style>".toRegex())) {
content content

@ -83,7 +83,12 @@ class RssSourceActivity : VMBaseActivity<RssSourceViewModel>(R.layout.activity_r
when (item.itemId) { when (item.itemId) {
R.id.menu_add -> startActivity<RssSourceEditActivity>() R.id.menu_add -> startActivity<RssSourceEditActivity>()
R.id.menu_import_source_local -> FilePicker R.id.menu_import_source_local -> FilePicker
.selectFile(this, importRequestCode, "text/*", arrayOf("txt", "json")) .selectFile(
this,
importRequestCode,
type = "text/*",
allowExtensions = arrayOf("txt", "json")
)
R.id.menu_import_source_onLine -> showImportDialog() R.id.menu_import_source_onLine -> showImportDialog()
R.id.menu_import_source_qr -> startActivityForResult<QrCodeActivity>(qrRequestCode) R.id.menu_import_source_qr -> startActivityForResult<QrCodeActivity>(qrRequestCode)
R.id.menu_group_manage -> GroupManageDialog() R.id.menu_group_manage -> GroupManageDialog()

@ -0,0 +1,67 @@
package io.legado.app.ui.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Typeface
import android.text.StaticLayout
import android.text.TextPaint
import android.util.AttributeSet
import android.view.View
import androidx.annotation.ColorInt
import io.legado.app.R
import io.legado.app.utils.dp
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.sp
class BatteryView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private var battery = 100
private val textPaint = TextPaint()
private var batteryHeight: Int = 0
private var batteryWidth: Int = 0
private val outFrame = Rect()
private val polar = Rect()
init {
textPaint.textSize = 10.sp.toFloat()
textPaint.strokeWidth = 1.dp.toFloat()
textPaint.isAntiAlias = true
textPaint.textAlign = Paint.Align.CENTER
textPaint.color = context.getCompatColor(R.color.tv_text_default)
textPaint.typeface = Typeface.createFromAsset(context.assets, "number.ttf")
batteryHeight = with(textPaint.fontMetrics) { descent - ascent + leading }.toInt()
batteryWidth = StaticLayout.getDesiredWidth("100", textPaint).toInt() + 10.dp
outFrame.set(1.dp, 1.dp, batteryWidth - 3.dp, batteryHeight - 1.dp)
polar.set(outFrame.right, batteryHeight / 3, batteryWidth, batteryHeight * 2 / 3)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(
MeasureSpec.makeMeasureSpec(batteryWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(batteryHeight, MeasureSpec.EXACTLY)
)
}
fun setColor(@ColorInt color: Int) {
textPaint.color = color
invalidate()
}
fun setBattery(battery: Int) {
this.battery = battery
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
textPaint.style = Paint.Style.STROKE
canvas.drawRect(outFrame, textPaint)
textPaint.style = Paint.Style.FILL
canvas.drawRect(polar, textPaint)
val text = battery.toString()
val baseHeight = batteryHeight - textPaint.fontMetrics.descent
canvas.drawText(text, outFrame.right / 2.toFloat(), baseHeight, textPaint)
}
}

@ -9,6 +9,7 @@ import io.legado.app.utils.invisible
import io.legado.app.utils.visible import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_font.view.* import kotlinx.android.synthetic.main.item_font.view.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.toast
import java.io.File import java.io.File
class FontAdapter(context: Context, val callBack: CallBack) : class FontAdapter(context: Context, val callBack: CallBack) :
@ -16,8 +17,12 @@ class FontAdapter(context: Context, val callBack: CallBack) :
override fun convert(holder: ItemViewHolder, item: File, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: File, payloads: MutableList<Any>) {
with(holder.itemView) { with(holder.itemView) {
val typeface = Typeface.createFromFile(item) try {
tv_font.typeface = typeface val typeface = Typeface.createFromFile(item)
tv_font.typeface = typeface
} catch (e: Exception) {
context.toast("读取${item.name}字体失败")
}
tv_font.text = item.name tv_font.text = item.name
this.onClick { callBack.onClick(item) } this.onClick { callBack.onClick(item) }
if (item.name == callBack.curFilePath.substringAfterLast(File.separator)) { if (item.name == callBack.curFilePath.substringAfterLast(File.separator)) {

@ -40,15 +40,21 @@ fun View.disableAutoFill() = run {
} }
fun View.gone() { fun View.gone() {
visibility = GONE if (visibility != GONE) {
visibility = GONE
}
} }
fun View.invisible() { fun View.invisible() {
visibility = INVISIBLE if (visibility != INVISIBLE) {
visibility = INVISIBLE
}
} }
fun View.visible() { fun View.visible() {
visibility = VISIBLE if (visibility != VISIBLE) {
visibility = VISIBLE
}
} }
fun View.visible(visible: Boolean) { fun View.visible(visible: Boolean) {

@ -47,10 +47,10 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
"/getSource" -> returnData = SourceController().getSource(parameters) "/getSource" -> returnData = SourceController().getSource(parameters)
"/getSources" -> returnData = SourceController().sources "/getSources" -> returnData = SourceController().sources
"/getBookshelf" -> returnData = BookshelfController().bookshelf "/getBookshelf" -> returnData = BookshelfController().bookshelf
"/getChapterList" -> returnData = "/getChapterList" ->
BookshelfController().getChapterList(parameters) returnData = BookshelfController().getChapterList(parameters)
"/getBookContent" -> returnData = "/getBookContent" ->
BookshelfController().getBookContent(parameters) returnData = BookshelfController().getBookContent(parameters)
} }
} }
} }

@ -21,41 +21,41 @@ class BookshelfController {
} }
fun getChapterList(parameters: Map<String, List<String>>): ReturnData { fun getChapterList(parameters: Map<String, List<String>>): ReturnData {
val strings = parameters["url"] val bookUrl = parameters["url"]?.getOrNull(0)
val returnData = ReturnData() val returnData = ReturnData()
if (strings == null) { if (bookUrl.isNullOrEmpty()) {
return returnData.setErrorMsg("参数url不能为空,请指定书籍地址") return returnData.setErrorMsg("参数url不能为空,请指定书籍地址")
} }
val chapterList = App.db.bookChapterDao().getChapterList(strings[0]) val chapterList = App.db.bookChapterDao().getChapterList(bookUrl)
return returnData.setData(chapterList) return returnData.setData(chapterList)
} }
fun getBookContent(parameters: Map<String, List<String>>): ReturnData { fun getBookContent(parameters: Map<String, List<String>>): ReturnData {
val strings = parameters["url"] val bookUrl = parameters["url"]?.getOrNull(0)
val index = parameters["index"]?.getOrNull(0)?.toInt()
val returnData = ReturnData() val returnData = ReturnData()
if (strings == null) { if (bookUrl.isNullOrEmpty()) {
return returnData.setErrorMsg("参数url不能为空,请指定内容地址") return returnData.setErrorMsg("参数url不能为空,请指定书籍地址")
}
if (index == null) {
return returnData.setErrorMsg("参数index不能为空, 请指定目录序号")
} }
val book = App.db.bookDao().getBook(strings[0]) val book = App.db.bookDao().getBook(bookUrl)
val chapter = App.db.bookChapterDao().getChapter(strings[0], strings[1].toInt()) val chapter = App.db.bookChapterDao().getChapter(bookUrl, index)
if (book == null || chapter == null) { if (book == null || chapter == null) {
returnData.setErrorMsg("未找到") returnData.setErrorMsg("未找到")
} else { } else {
val content = BookHelp.getContent(book, chapter) val content: String? = BookHelp.getContent(book, chapter)
if (content != null) { if (content != null) {
returnData.setData(content) returnData.setData(content)
} else { } else {
runBlocking { App.db.bookSourceDao().getBookSource(book.origin)?.let { source ->
App.db.bookSourceDao().getBookSource(book.origin)?.let { source -> runBlocking {
WebBook(source).getContent(book, chapter) WebBook(source).getContentSuspend(book, chapter)
.onSuccess { }.let {
returnData.setData(it!!) returnData.setData(it)
} }
.onError { } ?: returnData.setErrorMsg("未找到书源")
returnData.setErrorMsg(it.localizedMessage)
}
} ?: returnData.setErrorMsg("未找到书源")
}
} }
} }
return returnData return returnData

@ -58,12 +58,12 @@ class SourceController {
} }
fun getSource(parameters: Map<String, List<String>>): ReturnData { fun getSource(parameters: Map<String, List<String>>): ReturnData {
val strings = parameters["url"] val url = parameters["url"]?.getOrNull(0)
val returnData = ReturnData() val returnData = ReturnData()
if (strings == null) { if (url.isNullOrEmpty()) {
return returnData.setErrorMsg("参数url不能为空,请指定书源地址") return returnData.setErrorMsg("参数url不能为空,请指定书源地址")
} }
val bookSource = App.db.bookSourceDao().getBookSource(strings[0]) val bookSource = App.db.bookSourceDao().getBookSource(url)
?: return returnData.setErrorMsg("未找到书源,请检查书源地址") ?: return returnData.setErrorMsg("未找到书源,请检查书源地址")
return returnData.setData(bookSource) return returnData.setData(bookSource)
} }

@ -1,17 +1,30 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<io.legado.app.ui.widget.TitleBar <LinearLayout
android:id="@+id/title_bar" android:id="@+id/ll_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="match_parent"
android:orientation="vertical">
<WebView <io.legado.app.ui.widget.TitleBar
android:id="@+id/web_view" android:id="@+id/title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<FrameLayout
android:id="@+id/custom_web_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</LinearLayout> </FrameLayout>

@ -13,13 +13,33 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<io.legado.app.ui.widget.text.AccentTextView <LinearLayout
android:id="@+id/tv_header_padding"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:textSize="18sp" android:gravity="center_vertical"
android:text="@string/header" /> android:orientation="horizontal">
<io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/tv_header_padding"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="18sp"
android:text="@string/header" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/showLine" />
<io.legado.app.ui.widget.checkbox.SmoothCheckBox
android:id="@+id/cb_show_top_line"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_margin="6dp" />
</LinearLayout>
<io.legado.app.ui.widget.DetailSeekBar <io.legado.app.ui.widget.DetailSeekBar
android:id="@+id/dsb_header_padding_top" android:id="@+id/dsb_header_padding_top"
@ -109,7 +129,7 @@
android:text="@string/showLine" /> android:text="@string/showLine" />
<io.legado.app.ui.widget.checkbox.SmoothCheckBox <io.legado.app.ui.widget.checkbox.SmoothCheckBox
android:id="@+id/cb_show_line" android:id="@+id/cb_show_bottom_line"
android:layout_width="20dp" android:layout_width="20dp"
android:layout_height="20dp" android:layout_height="20dp"
android:layout_margin="6dp" /> android:layout_margin="6dp" />

@ -4,7 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<io.legado.app.ui.widget.recycler.RecyclerViewAtViewPager2 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_bookshelf" android:id="@+id/rv_bookshelf"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android" <androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/page_panel" android:id="@+id/page_panel"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -23,7 +24,9 @@
android:id="@+id/tv_top_right" android:id="@+id/tv_top_right"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="12sp" /> android:layout_marginLeft="3dp"
android:textSize="12sp"
tools:ignore="RtlHardcoded,RtlSymmetry" />
</LinearLayout> </LinearLayout>
@ -51,6 +54,7 @@
android:id="@+id/ll_footer" android:id="@+id/ll_footer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
@ -65,7 +69,16 @@
android:id="@+id/tv_bottom_right" android:id="@+id/tv_bottom_right"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="12sp" /> android:layout_marginLeft="3dp"
android:textSize="12sp"
tools:ignore="RtlHardcoded,RtlSymmetry" />
<io.legado.app.ui.widget.BatteryView
android:id="@+id/battery_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
tools:ignore="RtlHardcoded,RtlSymmetry" />
</LinearLayout> </LinearLayout>

@ -27,7 +27,6 @@
android:paddingLeft="10dp" android:paddingLeft="10dp"
android:paddingRight="10dp" android:paddingRight="10dp"
android:singleLine="true" android:singleLine="true"
android:text="@string/chapter"
android:textSize="12sp" android:textSize="12sp"
android:visibility="gone" /> android:visibility="gone" />
@ -38,7 +37,6 @@
android:paddingLeft="10dp" android:paddingLeft="10dp"
android:paddingRight="10dp" android:paddingRight="10dp"
android:singleLine="true" android:singleLine="true"
android:text="@string/chapter"
android:textSize="12sp" android:textSize="12sp"
android:visibility="gone" /> android:visibility="gone" />

@ -14,6 +14,10 @@
android:id="@+id/menu_group_manage" android:id="@+id/menu_group_manage"
android:title="@string/group_manage" /> android:title="@string/group_manage" />
<item
android:id="@+id/menu_no_group"
android:title="@string/no_group" />
<item <item
android:id="@+id/menu_all" android:id="@+id/menu_all"
android:title="@string/all" /> android:title="@string/all" />

@ -10,6 +10,13 @@
app:showAsAction="always" app:showAsAction="always"
tools:ignore="AlwaysShowAction" /> tools:ignore="AlwaysShowAction" />
<item
android:id="@+id/menu_group_all"
android:title="@string/all"
android:checkable="true"
android:checked="true"
app:showAsAction="never" />
<item <item
android:id="@+id/menu_group_local" android:id="@+id/menu_group_local"
android:title="@string/local" android:title="@string/local"

@ -639,4 +639,6 @@
<string name="title_center">居中</string> <string name="title_center">居中</string>
<string name="title_hide">隐藏</string> <string name="title_hide">隐藏</string>
<string name="add_to_group">加入分组</string> <string name="add_to_group">加入分组</string>
<string name="save_image">保存图片</string>
<string name="no_default_path">没有默认路径</string>
</resources> </resources>

@ -30,12 +30,6 @@
android:summary="537954522 点击复制" android:summary="537954522 点击复制"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<androidx.preference.Preference
android:key="zfbSk"
android:title="支付宝收款,支持红包"
android:summary="点击直接跳转支付宝"
app:iconSpaceReserved="false" />
<androidx.preference.Preference <androidx.preference.Preference
android:key="zfbHbRwm" android:key="zfbHbRwm"
android:title="支付宝红包二维码" android:title="支付宝红包二维码"

Loading…
Cancel
Save