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

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

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

@ -1,11 +1,14 @@
var $ = document.querySelector.bind(document)
, $$ = document.querySelectorAll.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"
, books
;
var now_chapter = -1;
var sum_chapter = 0;
var formatTime = value => {
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"
@ -13,13 +16,13 @@ var formatTime = value => {
};
var apiMap = {
getBookshelf: "/getBookshelf",
getChapterList: "/getChapterList",
getBookContent: "/getBookContent",
saveBook: "/saveBook"
"getBookshelf": "/getBookshelf",
"getChapterList": "/getChapterList",
"getBookContent": "/getBookContent",
"saveBook": "/saveBook"
};
var apiAddress = (apiName, url) => {
var apiAddress = (apiName, url, index) => {
let address = $('#address').value || window.location.host;
if (!(/^http|^\/\//).test(address)) {
address = "//" + address;
@ -28,6 +31,9 @@ var apiAddress = (apiName, url) => {
address += ":1122";
}
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) : "");
};
@ -41,12 +47,12 @@ var init = () => {
alert(getBookshelf.errorMsg);
return;
}
books = data.data.sort((book1, book2) => book1.serialNumber - book2.serialNumber);
books.forEach(book => {
books = data.data;
books.forEach((book, i) => {
let bookDiv = $c("div");
let img = $c("img");
img.src = book.coverUrl || randomImg;
img.setAttribute("data-series-num", book.serialNumber);
img.setAttribute("data-series-num", i);
bookDiv.appendChild(img);
bookDiv.innerHTML += `<table><tbody>
<tr><td>书名</td><td>${book.name}</td></tr>
@ -59,6 +65,8 @@ var init = () => {
});
$$('#books img').forEach(bookImg =>
bookImg.addEventListener("click", () => {
now_chapter = -1;
sum_chapter = 0;
$('#allcontent').classList.add("read");
var book = books[bookImg.getAttribute("data-series-num")];
$("#info").innerHTML = `<img src="${bookImg.src}">
@ -85,11 +93,13 @@ var init = () => {
data.data.forEach(chapter => {
let ch = $c("button");
ch.setAttribute("data-url", chapter.durChapterUrl);
ch.setAttribute("title", chapter.durChapterName);
ch.innerHTML = chapter.durChapterName.length > 15 ? chapter.durChapterName.substring(0, 14) + "..." : chapter.durChapterName;
ch.setAttribute("data-url", chapter.bookUrl);
ch.setAttribute("data-index", chapter.index);
ch.setAttribute("title", chapter.title);
ch.innerHTML = chapter.title.length > 15 ? chapter.title.substring(0, 14) + "..." : chapter.title;
$("#chapter").appendChild(ch);
});
sum_chapter = data.data.length;
$('#chapter').scrollTop = 0;
$("#content").innerHTML = "章节列表加载完成!";
});
@ -126,15 +136,47 @@ $('#showchapter').addEventListener("click", () => {
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) => {
if (e.target.tagName === "BUTTON") {
var url = e.target.getAttribute("data-url");
var index = e.target.getAttribute("data-index");
var name = e.target.getAttribute("title");
if (!url) {
alert("未取得章节地址");
alert("未取得书籍地址");
}
if (!index && (0 != index)) {
alert("未取得章节索引");
}
now_chapter = parseInt(index);
$("#content").innerHTML = "<p>" + name + " 加载中...</p>";
fetch(apiAddress("getBookContent", url), { mode: "cors" })
fetch(apiAddress("getBookContent", url, index), { mode: "cors" })
.then(res => res.json())
.then(data => {
if (!data.isSuccess) {

@ -1,13 +1,11 @@
package io.legado.app
import android.app.Activity
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import com.jeremyliao.liveeventbus.LiveEventBus
@ -56,7 +54,7 @@ class App : Application() {
.lifecycleObserverAlwaysActive(true)
.autoClear(false)
registerActivityLife()
registerActivityLifecycleCallbacks(ActivityHelp)
}
override fun onConfigurationChanged(newConfig: Configuration) {
@ -116,9 +114,7 @@ class App : Application() {
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun createChannelId() {
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
notificationManager?.let {
(getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager)?.let {
//用唯一的ID创建渠道对象
val downloadChannel = NotificationChannel(
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.R
import io.legado.app.data.entities.BookGroup
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.putPrefBoolean
import java.text.SimpleDateFormat
import javax.script.ScriptEngine
import javax.script.ScriptEngineManager
@ -29,14 +27,18 @@ object AppConst {
ScriptEngineManager().getEngineByName("rhino")
}
val TIME_FORMAT: SimpleDateFormat by lazy {
val timeFormat: SimpleDateFormat by lazy {
SimpleDateFormat("HH:mm")
}
val DATE_FORMAT: SimpleDateFormat by lazy {
val dateFormat: SimpleDateFormat by lazy {
SimpleDateFormat("yyyy/MM/dd HH:mm")
}
val fileNameFormat: SimpleDateFormat by lazy {
SimpleDateFormat("yy-MM-dd-HH-mm-ss")
}
val keyboardToolChars: List<String> by lazy {
arrayListOf(
"@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", "$", "#", "!", ".",
@ -48,18 +50,6 @@ object AppConst {
val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local))
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 notificationIdAudio = 1144772
const val notificationIdWeb = 1144773

@ -27,6 +27,9 @@ interface BookDao {
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
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||'%'")
fun liveDataSearch(key: String): LiveData<List<Book>>

@ -1,13 +1,15 @@
package io.legado.app.help
import android.app.Activity
import android.app.Application
import android.os.Bundle
import java.lang.ref.WeakReference
import java.util.*
/**
* Activity管理器,管理项目中Activity的状态
*/
object ActivityHelp {
object ActivityHelp : Application.ActivityLifecycleCallbacks {
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) {
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 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.utils.EncoderUtils
import io.legado.app.utils.MD5Utils
@ -50,6 +50,6 @@ interface JsExtensions {
}
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
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
get() = if (shareLayout) shareConfig.showFooterLine else durConfig.showFooterLine
set(value) =
@ -288,6 +294,7 @@ object ReadBookConfig {
var footerPaddingLeft: Int = 16,
var footerPaddingRight: Int = 16,
var footerPaddingTop: Int = 6,
var showHeaderLine: Boolean = false,
var showFooterLine: Boolean = true
) {
fun setBg(bgType: Int, bg: String) {

@ -57,6 +57,16 @@ object HttpHelper {
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 {
return getRetrofit(baseUrl, encode).create(T::class.java)
}

@ -84,8 +84,8 @@ class WebBook(val bookSource: BookSource) {
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<Book> {
book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) {
book.type = bookSource.bookSourceType
val body =
if (!book.infoHtml.isNullOrEmpty()) {
book.infoHtml
@ -111,8 +111,8 @@ class WebBook(val bookSource: BookSource) {
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<List<BookChapter>> {
book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) {
book.type = bookSource.bookSourceType
val body =
if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {
book.tocHtml
@ -139,36 +139,50 @@ class WebBook(val bookSource: BookSource) {
context: CoroutineContext = Dispatchers.IO
): Coroutine<String> {
return Coroutine.async(scope, context) {
if (bookSource.getContentRule().content.isNullOrEmpty()) {
Debug.log(sourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}")
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
getContentSuspend(
book, bookChapter, nextChapterUrl, scope
)
}
}
/**
* 章节内容
*/
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 var runnable: Runnable = Runnable { upDownload() }
private val downloadMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private val downloadCount = hashMapOf<String, DownloadCount>();
private val finalMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private var notificationContent = "正在启动下载"
private val notificationBuilder by lazy {
val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload)
.setSmallIcon(R.drawable.ic_download)
@ -97,6 +99,11 @@ class DownloadService : BaseService() {
finalMap.remove(bookUrl)
}
private fun updateNotification(downloadCount:DownloadCount, totalCount: Int, content: String){
notificationContent =
"进度:${downloadCount.downloadFinishedCount}/$totalCount,成功:${downloadCount.successCount},$content"
}
private fun download() {
val task = Coroutine.async(this, context = searchPool) {
downloadMap.forEach { entry ->
@ -106,6 +113,9 @@ class DownloadService : BaseService() {
val bookSource =
App.db.bookSourceDao().getBookSource(book.origin) ?: return@async
val webBook = WebBook(bookSource)
downloadCount[entry.key] = DownloadCount()
entry.value.forEach { chapter ->
if (!isActive) return@async
if (downloadMap.containsKey(book.bookUrl)) {
@ -116,16 +126,19 @@ class DownloadService : BaseService() {
scope = this,
context = searchPool
)
.onStart {
notificationContent = chapter.title
}
//.onStart {
// notificationContent = "启动:" + chapter.title
//}
.onSuccess(IO) { content ->
content?.let {
downloadCount[entry.key]?.increaseSuccess()
BookHelp.saveContent(book, chapter, content)
}
}
.onFinally(IO) {
synchronized(this@DownloadService) {
downloadCount[entry.key]?.increaseFinished()
downloadCount[entry.key]?.let { updateNotification(it, entry.value.size, chapter.title) }
val chapterMap =
finalMap[book.bookUrl]
?: linkedSetOf<BookChapter>().apply {
@ -135,9 +148,14 @@ class DownloadService : BaseService() {
if (chapterMap.size == entry.value.size) {
downloadMap.remove(book.bookUrl)
finalMap.remove(book.bookUrl)
downloadCount.remove(entry.key)
}
}
}
} else{
//无需下载的,设置为增加成功
downloadCount[entry.key]?.increaseSuccess()
downloadCount[entry.key]?.increaseFinished()
}
}
}
@ -176,4 +194,17 @@ class DownloadService : BaseService() {
val notification = builder.build()
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
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.view.Menu
import android.view.MenuItem
import io.legado.app.R
import io.legado.app.base.BaseActivity
import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.openUrl
import kotlinx.android.synthetic.main.activity_about.*
import org.jetbrains.anko.share
class AboutActivity : BaseActivity(R.layout.activity_about) {
override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -17,6 +23,16 @@ class AboutActivity : BaseActivity(R.layout.activity_about) {
supportFragmentManager.beginTransaction()
.replace(R.id.fl_fragment, aboutFragment, fTag)
.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 {

@ -2,7 +2,6 @@ package io.legado.app.ui.about
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
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.sendToClip
import org.jetbrains.anko.longToast
import java.net.URLEncoder
class DonateFragment : PreferenceFragmentCompat() {
@ -36,7 +34,6 @@ class DonateFragment : PreferenceFragmentCompat() {
"zfbHbRwm" -> requireContext().openUrl(zfbHbRwmUrl)
"zfbSkRwm" -> requireContext().openUrl(zfbSkRwmUrl)
"qqSkRwm" -> requireContext().openUrl(qqSkRwmUrl)
"zfbSk" -> aliDonate(requireContext())
"zfbHbSsm" -> getZfbHb(requireContext())
"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()
-2 -> App.db.bookDao().observeLocal()
-3 -> App.db.bookDao().observeAudio()
-11 -> App.db.bookDao().observeNoGroup()
else -> App.db.bookDao().observeByGroup(groupId)
}
booksLiveData?.observe(this, Observer {
@ -112,6 +113,11 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
when (item.itemId) {
R.id.menu_group_manage -> GroupManageDialog()
.show(supportFragmentManager, "groupManage")
R.id.menu_no_group -> {
title_bar.subtitle = getString(R.string.no_group)
groupId = -11
initBookData()
}
R.id.menu_all -> {
title_bar.subtitle = item.title
groupId = -1

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

@ -20,9 +20,9 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.AppConst
import io.legado.app.constant.Theme
import io.legado.app.data.entities.BookGroup
import io.legado.app.help.AppConfig
import io.legado.app.help.ItemTouchCallback
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView
@ -42,7 +42,7 @@ import kotlin.collections.ArrayList
class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
private lateinit var viewModel: GroupViewModel
private lateinit var adapter: GroupAdapter
private var callBack: CallBack? = null
private val callBack: CallBack? get() = parentFragment as? CallBack
override fun onStart() {
super.onStart()
@ -62,19 +62,12 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callBack = parentFragment as? CallBack
tool_bar.title = getString(R.string.group_manage)
initData()
initMenu()
}
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())
recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
@ -90,17 +83,36 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
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 {
when (item?.itemId) {
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 -> {
item.isChecked = !item.isChecked
AppConst.bookGroupLocalShow = item.isChecked
AppConfig.bookGroupLocalShow = item.isChecked
callBack?.upGroup()
}
R.id.menu_group_audio -> {
item.isChecked = !item.isChecked
AppConst.bookGroupAudioShow = item.isChecked
AppConfig.bookGroupAudioShow = item.isChecked
callBack?.upGroup()
}
}

@ -87,7 +87,7 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) :
ll_brief.visible()
tv_tag.text = item.name.substringAfterLast(".")
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())
}
tv_name.text = item.name

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

@ -76,7 +76,8 @@ class PaddingConfigDialog : DialogFragment() {
dsb_footer_padding_bottom.progress = footerPaddingBottom
dsb_footer_padding_left.progress = footerPaddingLeft
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) {
@ -131,7 +132,13 @@ class PaddingConfigDialog : DialogFragment() {
footerPaddingRight = it
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) {
showFooterLine = isChecked
postEvent(EventBus.UP_CONFIG, true)

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

@ -270,8 +270,8 @@ object ChapterProvider {
contentPaint.isAntiAlias = true
contentPaint.color = ReadBookConfig.durConfig.textColor()
contentPaint.letterSpacing = ReadBookConfig.letterSpacing
val bold = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL
contentPaint.typeface = Typeface.create(typeface, bold)
val style = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL
contentPaint.typeface = Typeface.create(typeface, style)
contentPaint.textSize = ReadBookConfig.textSize.sp.toFloat()
//间距
lineSpacingExtra = ReadBookConfig.lineSpacingExtra
@ -294,5 +294,5 @@ object ChapterProvider {
}
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 com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap
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.ReadBookConfig
import io.legado.app.ui.book.read.page.entities.TextPage
@ -18,6 +18,8 @@ import java.util.*
class ContentView(context: Context) : FrameLayout(context) {
private var battery = 100
init {
//设置背景防止切换背景时文字重叠
setBackgroundColor(context.getCompatColor(R.color.background))
@ -26,8 +28,7 @@ class ContentView(context: Context) : FrameLayout(context) {
upStyle()
upTime()
content_text_view.upView = {
tv_bottom_left.text = it.title
setPageIndex(it.index, it.pageSize)
setProgress(it)
}
}
@ -60,6 +61,7 @@ class ContentView(context: Context) : FrameLayout(context) {
footerPaddingRight.dp,
footerPaddingBottom.dp
)
vw_top_divider.visible(showHeaderLine)
vw_bottom_divider.visible(showFooterLine)
content_text_view.upVisibleRect()
durConfig.textColor().let {
@ -67,6 +69,14 @@ class ContentView(context: Context) : FrameLayout(context) {
tv_top_right.setTextColor(it)
tv_bottom_left.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() {
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) {
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) {
tv_bottom_left.text = when (AppConfig.chineseConverterType) {
1 -> ZhConvertBootstrap.newInstance().toSimple(textPage.title)
2 -> ZhConvertBootstrap.newInstance().toTraditional(textPage.title)
else -> textPage.title
}
setPageIndex(textPage.index, textPage.pageSize)
setProgress(textPage)
content_text_view.resetPageOffset()
content_text_view.setContent(textPage)
}
@ -108,9 +118,19 @@ class ContentView(context: Context) : FrameLayout(context) {
}
@SuppressLint("SetTextI18n")
fun setPageIndex(pageIndex: Int?, pageSize: Int) {
pageIndex?.let {
tv_bottom_right.text = "${pageIndex.plus(1)}/${pageSize}"
fun setProgress(textPage: TextPage) = textPage.apply {
val title = when (AppConfig.chineseConverterType) {
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.R
import io.legado.app.ui.book.read.page.ChapterProvider
import java.text.DecimalFormat
data class TextPage(
var index: Int = 0,
@ -84,4 +85,20 @@ data class TextPage(
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
private fun upSearchItems(items: List<SearchBook>, isMandatoryUpdate: Boolean) {
if (!isMandatoryUpdate && System.currentTimeMillis() - refreshTime < 500) {
return
}
refreshTime = System.currentTimeMillis()
try {
if (isMandatoryUpdate || System.currentTimeMillis() - refreshTime > 500) {
refreshTime = System.currentTimeMillis()
val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), items))
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 ->
GroupManageDialog().show(supportFragmentManager, "groupManage")
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()
}
if (item.groupId == R.id.source_group) {

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

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

@ -1,37 +1,42 @@
package io.legado.app.ui.rss.read
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Bundle
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.view.*
import android.webkit.*
import androidx.core.view.size
import androidx.lifecycle.Observer
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.lib.theme.DrawableUtils
import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.getViewModel
import io.legado.app.utils.openUrl
import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.filechooser.FilePicker
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_rss_read.*
import kotlinx.coroutines.launch
import org.apache.commons.text.StringEscapeUtils
import org.jetbrains.anko.share
import org.jetbrains.anko.toast
import org.jsoup.Jsoup
import org.jsoup.safety.Whitelist
class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_read),
FileChooserDialog.CallBack,
ReadRssViewModel.CallBack {
override val viewModel: ReadRssViewModel
get() = getViewModel(ReadRssViewModel::class.java)
private val savePathRequestCode = 132
private val imagePathKey = ""
private var starMenuItem: MenuItem? = null
private var ttsMenuItem: MenuItem? = null
private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null
private var webPic: String? = null
override fun onActivityCreated(savedInstanceState: Bundle?) {
viewModel.callBack = this
@ -41,6 +46,20 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
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 {
menuInflater.inflate(R.menu.rss_read, menu)
return super.onCompatCreateOptionsMenu(menu)
@ -65,6 +84,20 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
}
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() {
override fun shouldOverrideUrlLoading(
view: WebView?,
@ -95,6 +128,30 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
domStorageEnabled = 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")
@ -113,7 +170,6 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
url
)//不想用baseUrl进else
} else {
//webView.loadData(html, "text/html;charset=utf-8", "utf-8")//经测试可以解决中文乱码
web_view.loadDataWithBaseURL(
null,
html,
@ -175,7 +231,10 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
event?.let {
when (keyCode) {
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()
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() {
super.onDestroy()
web_view.destroy()

@ -2,19 +2,31 @@ package io.legado.app.ui.rss.read
import android.app.Application
import android.content.Intent
import android.net.Uri
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import android.util.Base64
import android.webkit.URLUtil
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.MutableLiveData
import io.legado.app.App
import io.legado.app.R
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.RssSource
import io.legado.app.help.http.HttpHelper
import io.legado.app.model.Rss
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 java.io.File
import java.util.*
class ReadRssViewModel(application: Application) : BaseViewModel(application),
TextToSpeech.OnInitListener {
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 {
return if (content.contains("<style>".toRegex())) {
content

@ -83,7 +83,12 @@ class RssSourceActivity : VMBaseActivity<RssSourceViewModel>(R.layout.activity_r
when (item.itemId) {
R.id.menu_add -> startActivity<RssSourceEditActivity>()
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_qr -> startActivityForResult<QrCodeActivity>(qrRequestCode)
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 kotlinx.android.synthetic.main.item_font.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.toast
import java.io.File
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>) {
with(holder.itemView) {
val typeface = Typeface.createFromFile(item)
tv_font.typeface = typeface
try {
val typeface = Typeface.createFromFile(item)
tv_font.typeface = typeface
} catch (e: Exception) {
context.toast("读取${item.name}字体失败")
}
tv_font.text = item.name
this.onClick { callBack.onClick(item) }
if (item.name == callBack.curFilePath.substringAfterLast(File.separator)) {

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

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

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

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

@ -1,17 +1,30 @@
<?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_height="match_parent"
android:orientation="vertical">
<io.legado.app.ui.widget.TitleBar
android:id="@+id/title_bar"
<LinearLayout
android:id="@+id/ll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="match_parent"
android:orientation="vertical">
<WebView
android:id="@+id/web_view"
<io.legado.app.ui.widget.TitleBar
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_height="match_parent" />
</LinearLayout>
</FrameLayout>

@ -13,13 +13,33 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/tv_header_padding"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
android:textSize="18sp"
android:text="@string/header" />
android:gravity="center_vertical"
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
android:id="@+id/dsb_header_padding_top"
@ -109,7 +129,7 @@
android:text="@string/showLine" />
<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_height="20dp"
android:layout_margin="6dp" />

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

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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:layout_width="match_parent"
android:layout_height="match_parent"
@ -23,7 +24,9 @@
android:id="@+id/tv_top_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp" />
android:layout_marginLeft="3dp"
android:textSize="12sp"
tools:ignore="RtlHardcoded,RtlSymmetry" />
</LinearLayout>
@ -51,6 +54,7 @@
android:id="@+id/ll_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
@ -65,7 +69,16 @@
android:id="@+id/tv_bottom_right"
android:layout_width="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>

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

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

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

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

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

Loading…
Cancel
Save