commit
73469af493
@ -1,2 +1,3 @@ |
|||||||
# legado |
# legado |
||||||
## 阅读3.0 |
## 阅读3.0 |
||||||
|
书源规则 https://celeter.github.io/?tdsourcetag=s_pctim_aiomsg |
||||||
|
@ -0,0 +1,12 @@ |
|||||||
|
## 文件结构介绍 |
||||||
|
|
||||||
|
* base 基类 |
||||||
|
* constant 常量 |
||||||
|
* data 数据 |
||||||
|
* help 帮助 |
||||||
|
* lib 库 |
||||||
|
* model 解析 |
||||||
|
* receiver 广播侦听 |
||||||
|
* service 服务 |
||||||
|
* ui 界面 |
||||||
|
* web web服务 |
@ -1,6 +1,6 @@ |
|||||||
package io.legado.app.constant |
package io.legado.app.constant |
||||||
|
|
||||||
object Bus { |
object EventBus { |
||||||
const val MEDIA_BUTTON = "mediaButton" |
const val MEDIA_BUTTON = "mediaButton" |
||||||
const val RECREATE = "RECREATE" |
const val RECREATE = "RECREATE" |
||||||
const val UP_BOOK = "sourceDebugLog" |
const val UP_BOOK = "sourceDebugLog" |
@ -1,6 +1,6 @@ |
|||||||
package io.legado.app.constant |
package io.legado.app.constant |
||||||
|
|
||||||
object Action { |
object IntentAction { |
||||||
const val start = "start" |
const val start = "start" |
||||||
const val play = "play" |
const val play = "play" |
||||||
const val stop = "stop" |
const val stop = "stop" |
@ -1 +1,3 @@ |
|||||||
## 存储数据用 |
## 存储数据用 |
||||||
|
* dao 数据操作 |
||||||
|
* entities 数据模型 |
@ -0,0 +1,20 @@ |
|||||||
|
package io.legado.app.data.dao |
||||||
|
|
||||||
|
import androidx.room.* |
||||||
|
import io.legado.app.data.entities.TxtTocRule |
||||||
|
|
||||||
|
@Dao |
||||||
|
interface TxtTocRuleDao { |
||||||
|
|
||||||
|
@get:Query("select * from txtTocRules order by serialNumber") |
||||||
|
val all: List<TxtTocRule> |
||||||
|
|
||||||
|
@get:Query("select * from txtTocRules where enable = 1 order by serialNumber") |
||||||
|
val enabled: List<TxtTocRule> |
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE) |
||||||
|
fun insert(vararg rule: TxtTocRule) |
||||||
|
|
||||||
|
@Delete |
||||||
|
fun delete(vararg rule: TxtTocRule) |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
package io.legado.app.data.entities |
||||||
|
|
||||||
|
import androidx.room.Entity |
||||||
|
import androidx.room.PrimaryKey |
||||||
|
|
||||||
|
@Entity(tableName = "rssReadRecords") |
||||||
|
data class RssReadRecord(@PrimaryKey val record: String, val read: Boolean = true) |
@ -0,0 +1,14 @@ |
|||||||
|
package io.legado.app.data.entities |
||||||
|
|
||||||
|
import androidx.room.Entity |
||||||
|
import androidx.room.PrimaryKey |
||||||
|
|
||||||
|
|
||||||
|
@Entity(tableName = "txtTocRules") |
||||||
|
data class TxtTocRule( |
||||||
|
@PrimaryKey |
||||||
|
var name: String = "", |
||||||
|
var rule: String = "", |
||||||
|
var serialNumber: Int, |
||||||
|
var enable: Boolean = true |
||||||
|
) |
@ -0,0 +1,55 @@ |
|||||||
|
package io.legado.app.help.storage |
||||||
|
|
||||||
|
import android.util.Log |
||||||
|
import io.legado.app.App |
||||||
|
import io.legado.app.constant.AppConst |
||||||
|
import io.legado.app.data.entities.Book |
||||||
|
import io.legado.app.utils.readBool |
||||||
|
import io.legado.app.utils.readInt |
||||||
|
import io.legado.app.utils.readLong |
||||||
|
import io.legado.app.utils.readString |
||||||
|
|
||||||
|
object OldBook { |
||||||
|
|
||||||
|
fun toNewBook(json: String): List<Book> { |
||||||
|
val books = mutableListOf<Book>() |
||||||
|
val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$") |
||||||
|
val existingBooks = App.db.bookDao().allBookUrls.toSet() |
||||||
|
for (item in items) { |
||||||
|
val jsonItem = Restore.jsonPath.parse(item) |
||||||
|
val book = Book() |
||||||
|
book.bookUrl = jsonItem.readString("$.noteUrl") ?: "" |
||||||
|
if (book.bookUrl.isBlank()) continue |
||||||
|
book.name = jsonItem.readString("$.bookInfoBean.name") ?: "" |
||||||
|
if (book.bookUrl in existingBooks) { |
||||||
|
Log.d(AppConst.APP_TAG, "Found existing book: ${book.name}") |
||||||
|
continue |
||||||
|
} |
||||||
|
book.origin = jsonItem.readString("$.tag") ?: "" |
||||||
|
book.originName = jsonItem.readString("$.bookInfoBean.origin") ?: "" |
||||||
|
book.author = jsonItem.readString("$.bookInfoBean.author") ?: "" |
||||||
|
book.type = |
||||||
|
if (jsonItem.readString("$.bookInfoBean.bookSourceType") == "AUDIO") 1 else 0 |
||||||
|
book.tocUrl = jsonItem.readString("$.bookInfoBean.chapterUrl") ?: book.bookUrl |
||||||
|
book.coverUrl = jsonItem.readString("$.bookInfoBean.coverUrl") |
||||||
|
book.customCoverUrl = jsonItem.readString("$.customCoverPath") |
||||||
|
book.lastCheckTime = jsonItem.readLong("$.bookInfoBean.finalRefreshData") ?: 0 |
||||||
|
book.canUpdate = jsonItem.readBool("$.allowUpdate") == true |
||||||
|
book.totalChapterNum = jsonItem.readInt("$.chapterListSize") ?: 0 |
||||||
|
book.durChapterIndex = jsonItem.readInt("$.durChapter") ?: 0 |
||||||
|
book.durChapterTitle = jsonItem.readString("$.durChapterName") |
||||||
|
book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0 |
||||||
|
book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0 |
||||||
|
book.group = jsonItem.readInt("$.group") ?: 0 |
||||||
|
book.intro = jsonItem.readString("$.bookInfoBean.introduce") |
||||||
|
book.latestChapterTitle = jsonItem.readString("$.lastChapterName") |
||||||
|
book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0 |
||||||
|
book.order = jsonItem.readInt("$.serialNumber") ?: 0 |
||||||
|
book.useReplaceRule = jsonItem.readBool("$.useReplaceRule") == true |
||||||
|
book.variable = jsonItem.readString("$.variable") |
||||||
|
books.add(book) |
||||||
|
} |
||||||
|
return books |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
package io.legado.app.model.localBook |
||||||
|
|
||||||
|
import androidx.documentfile.provider.DocumentFile |
||||||
|
import io.legado.app.App |
||||||
|
import io.legado.app.data.entities.Book |
||||||
|
|
||||||
|
|
||||||
|
object LocalBook { |
||||||
|
|
||||||
|
fun importFile(doc: DocumentFile) { |
||||||
|
doc.name?.let { fileName -> |
||||||
|
val str = fileName.substringBeforeLast(".") |
||||||
|
var name = str.substringBefore("作者") |
||||||
|
val author = str.substringAfter("作者", "") |
||||||
|
val smhStart = name.indexOf("《") |
||||||
|
val smhEnd = name.indexOf("》") |
||||||
|
if (smhStart != -1 && smhEnd != -1) { |
||||||
|
name = (name.substring(smhStart + 1, smhEnd)) |
||||||
|
} |
||||||
|
val book = Book( |
||||||
|
bookUrl = doc.uri.toString(), |
||||||
|
name = name, |
||||||
|
author = author, |
||||||
|
originName = fileName |
||||||
|
) |
||||||
|
App.db.bookDao().insert(book) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1 +1,7 @@ |
|||||||
## android服务 |
## android服务 |
||||||
|
* AudioPlayService 音频播放服务 |
||||||
|
* CheckSourceService 书源检测服务 |
||||||
|
* DownloadService 缓存服务 |
||||||
|
* HttpReadAloudService 在线朗读服务 |
||||||
|
* TTSReadAloudService tts朗读服务 |
||||||
|
* WebService web服务 |
@ -0,0 +1,35 @@ |
|||||||
|
package io.legado.app.service.help |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import android.content.Intent |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.constant.IntentAction |
||||||
|
import io.legado.app.data.entities.BookSource |
||||||
|
import io.legado.app.service.CheckSourceService |
||||||
|
import org.jetbrains.anko.toast |
||||||
|
|
||||||
|
object CheckSource { |
||||||
|
|
||||||
|
fun start(context: Context, sources: LinkedHashSet<BookSource>) { |
||||||
|
if (sources.isEmpty()) { |
||||||
|
context.toast(R.string.non_select) |
||||||
|
return |
||||||
|
} |
||||||
|
val selectedIds: ArrayList<String> = arrayListOf() |
||||||
|
sources.map { |
||||||
|
selectedIds.add(it.bookSourceUrl) |
||||||
|
} |
||||||
|
Intent(context, CheckSourceService::class.java).let { |
||||||
|
it.action = IntentAction.start |
||||||
|
it.putExtra("selectIds", selectedIds) |
||||||
|
context.startService(it) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun stop(context: Context) { |
||||||
|
Intent(context, CheckSourceService::class.java).let { |
||||||
|
it.action = IntentAction.stop |
||||||
|
context.startService(it) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
package io.legado.app.ui.book.arrange |
||||||
|
|
||||||
|
import android.os.Bundle |
||||||
|
import android.view.Menu |
||||||
|
import android.view.MenuItem |
||||||
|
import androidx.lifecycle.LiveData |
||||||
|
import androidx.lifecycle.Observer |
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager |
||||||
|
import io.legado.app.App |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.base.VMBaseActivity |
||||||
|
import io.legado.app.data.entities.Book |
||||||
|
import io.legado.app.data.entities.BookGroup |
||||||
|
import io.legado.app.lib.dialogs.alert |
||||||
|
import io.legado.app.lib.dialogs.okButton |
||||||
|
import io.legado.app.lib.theme.ATH |
||||||
|
import io.legado.app.utils.applyTint |
||||||
|
import io.legado.app.utils.getViewModel |
||||||
|
import kotlinx.android.synthetic.main.activity_arrange_book.* |
||||||
|
import org.jetbrains.anko.sdk27.listeners.onClick |
||||||
|
import org.jetbrains.anko.toast |
||||||
|
|
||||||
|
|
||||||
|
class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activity_arrange_book), |
||||||
|
ArrangeBookAdapter.CallBack { |
||||||
|
override val viewModel: ArrangeBookViewModel |
||||||
|
get() = getViewModel(ArrangeBookViewModel::class.java) |
||||||
|
override val groupList: ArrayList<BookGroup> = arrayListOf() |
||||||
|
private lateinit var adapter: ArrangeBookAdapter |
||||||
|
private var groupLiveData: LiveData<List<BookGroup>>? = null |
||||||
|
private var booksLiveData: LiveData<List<Book>>? = null |
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) { |
||||||
|
initView() |
||||||
|
initData() |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { |
||||||
|
menuInflater.inflate(R.menu.arrange_book, menu) |
||||||
|
return super.onCompatCreateOptionsMenu(menu) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { |
||||||
|
when (item.itemId) { |
||||||
|
|
||||||
|
} |
||||||
|
return super.onCompatOptionsItemSelected(item) |
||||||
|
} |
||||||
|
|
||||||
|
private fun initView() { |
||||||
|
ATH.applyEdgeEffectColor(recycler_view) |
||||||
|
recycler_view.layoutManager = LinearLayoutManager(this) |
||||||
|
adapter = ArrangeBookAdapter(this, this) |
||||||
|
recycler_view.adapter = adapter |
||||||
|
cb_selected_all.onClick { |
||||||
|
adapter.selectAll(!adapter.isSelectAll()) |
||||||
|
} |
||||||
|
btn_delete.onClick { |
||||||
|
if (adapter.selectedBooks.isEmpty()) { |
||||||
|
toast(R.string.non_select) |
||||||
|
return@onClick |
||||||
|
} |
||||||
|
alert(titleResource = R.string.sure, messageResource = R.string.sure_del) { |
||||||
|
okButton { |
||||||
|
viewModel.deleteBook(*adapter.selectedBooks.toTypedArray()) |
||||||
|
} |
||||||
|
}.show().applyTint() |
||||||
|
} |
||||||
|
btn_to_group.onClick { |
||||||
|
if (adapter.selectedBooks.isEmpty()) { |
||||||
|
toast(R.string.non_select) |
||||||
|
return@onClick |
||||||
|
} |
||||||
|
selectGroup() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun initData() { |
||||||
|
groupLiveData?.removeObservers(this) |
||||||
|
groupLiveData = App.db.bookGroupDao().liveDataAll() |
||||||
|
groupLiveData?.observe(this, Observer { |
||||||
|
groupList.clear() |
||||||
|
groupList.addAll(it) |
||||||
|
adapter.notifyDataSetChanged() |
||||||
|
}) |
||||||
|
booksLiveData?.removeObservers(this) |
||||||
|
booksLiveData = App.db.bookDao().observeAll() |
||||||
|
booksLiveData?.observe(this, Observer { |
||||||
|
adapter.setItems(it) |
||||||
|
upSelectCount() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
override fun selectGroup() { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
override fun upSelectCount() { |
||||||
|
cb_selected_all.isChecked = adapter.isSelectAll() |
||||||
|
//重置全选的文字 |
||||||
|
if (cb_selected_all.isChecked) { |
||||||
|
cb_selected_all.setText(R.string.cancel) |
||||||
|
} else { |
||||||
|
cb_selected_all.setText(R.string.select_all) |
||||||
|
} |
||||||
|
tv_select_count.text = |
||||||
|
getString(R.string.select_count, adapter.selectedBooks.size, adapter.getItems().size) |
||||||
|
} |
||||||
|
|
||||||
|
override fun deleteBook(book: Book) { |
||||||
|
alert(titleResource = R.string.sure, messageResource = R.string.sure_del) { |
||||||
|
okButton { |
||||||
|
viewModel.deleteBook(book) |
||||||
|
} |
||||||
|
}.show().applyTint() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,87 @@ |
|||||||
|
package io.legado.app.ui.book.arrange |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.base.adapter.ItemViewHolder |
||||||
|
import io.legado.app.base.adapter.SimpleRecyclerAdapter |
||||||
|
import io.legado.app.data.entities.Book |
||||||
|
import io.legado.app.data.entities.BookGroup |
||||||
|
import kotlinx.android.synthetic.main.item_arrange_book.view.* |
||||||
|
import org.jetbrains.anko.sdk27.listeners.onClick |
||||||
|
|
||||||
|
|
||||||
|
class ArrangeBookAdapter(context: Context, val callBack: CallBack) : |
||||||
|
SimpleRecyclerAdapter<Book>(context, R.layout.item_arrange_book) { |
||||||
|
|
||||||
|
val selectedBooks: HashSet<Book> = hashSetOf() |
||||||
|
|
||||||
|
fun isSelectAll(): Boolean { |
||||||
|
return if (selectedBooks.isEmpty()) { |
||||||
|
false |
||||||
|
} else { |
||||||
|
selectedBooks.size >= itemCount |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun selectAll(selectAll: Boolean) { |
||||||
|
if (selectAll) { |
||||||
|
getItems().forEach { |
||||||
|
selectedBooks.add(it) |
||||||
|
} |
||||||
|
notifyDataSetChanged() |
||||||
|
callBack.upSelectCount() |
||||||
|
} else { |
||||||
|
selectedBooks.clear() |
||||||
|
notifyDataSetChanged() |
||||||
|
callBack.upSelectCount() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) { |
||||||
|
with(holder.itemView) { |
||||||
|
tv_name.text = item.name |
||||||
|
tv_author.text = context.getString(R.string.author_show, item.author) |
||||||
|
tv_group.text = getGroupName(item.group) |
||||||
|
checkbox.isChecked = selectedBooks.contains(item) |
||||||
|
checkbox.onClick { |
||||||
|
if (checkbox.isChecked) { |
||||||
|
selectedBooks.add(item) |
||||||
|
} else { |
||||||
|
selectedBooks.remove(item) |
||||||
|
} |
||||||
|
callBack.upSelectCount() |
||||||
|
} |
||||||
|
onClick { |
||||||
|
checkbox.isChecked = !checkbox.isChecked |
||||||
|
if (checkbox.isChecked) { |
||||||
|
selectedBooks.add(item) |
||||||
|
} else { |
||||||
|
selectedBooks.remove(item) |
||||||
|
} |
||||||
|
callBack.upSelectCount() |
||||||
|
} |
||||||
|
tv_delete.onClick { |
||||||
|
callBack.deleteBook(item) |
||||||
|
} |
||||||
|
tv_group.onClick { |
||||||
|
callBack.selectGroup() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun getGroupName(groupId: Int): String { |
||||||
|
callBack.groupList.forEach { |
||||||
|
if (it.groupId == groupId) { |
||||||
|
return it.groupName |
||||||
|
} |
||||||
|
} |
||||||
|
return context.getString(R.string.group) |
||||||
|
} |
||||||
|
|
||||||
|
interface CallBack { |
||||||
|
val groupList: List<BookGroup> |
||||||
|
fun upSelectCount() |
||||||
|
fun deleteBook(book: Book) |
||||||
|
fun selectGroup() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
package io.legado.app.ui.book.arrange |
||||||
|
|
||||||
|
import android.app.Application |
||||||
|
import io.legado.app.App |
||||||
|
import io.legado.app.base.BaseViewModel |
||||||
|
import io.legado.app.data.entities.Book |
||||||
|
|
||||||
|
|
||||||
|
class ArrangeBookViewModel(application: Application) : BaseViewModel(application) { |
||||||
|
|
||||||
|
|
||||||
|
fun deleteBook(vararg book: Book) { |
||||||
|
execute { |
||||||
|
App.db.bookDao().delete() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1,12 +1,77 @@ |
|||||||
package io.legado.app.ui.changecover |
package io.legado.app.ui.changecover |
||||||
|
|
||||||
import android.app.Application |
import android.app.Application |
||||||
|
import androidx.lifecycle.MutableLiveData |
||||||
|
import io.legado.app.App |
||||||
import io.legado.app.base.BaseViewModel |
import io.legado.app.base.BaseViewModel |
||||||
|
import io.legado.app.help.AppConfig |
||||||
|
import io.legado.app.help.coroutine.Coroutine |
||||||
|
import io.legado.app.model.WebBook |
||||||
|
import kotlinx.coroutines.Dispatchers |
||||||
|
import kotlinx.coroutines.asCoroutineDispatcher |
||||||
|
import kotlinx.coroutines.withContext |
||||||
|
import java.util.concurrent.Executors |
||||||
|
|
||||||
class ChangeCoverViewModel(application: Application) : BaseViewModel(application) { |
class ChangeCoverViewModel(application: Application) : BaseViewModel(application) { |
||||||
|
private var searchPool = |
||||||
|
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() |
||||||
|
var callBack: CallBack? = null |
||||||
var name: String = "" |
var name: String = "" |
||||||
var author: String = "" |
var author: String = "" |
||||||
|
private var task: Coroutine<*>? = null |
||||||
|
val searchStateData = MutableLiveData<Boolean>() |
||||||
|
|
||||||
|
fun initData() { |
||||||
|
execute { |
||||||
|
App.db.searchBookDao().getEnableHasCover(name, author) |
||||||
|
}.onSuccess { |
||||||
|
it?.let { |
||||||
|
callBack?.adapter?.setItems(it) |
||||||
|
} |
||||||
|
}.onFinally { |
||||||
|
search() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun search() { |
||||||
|
task = execute { |
||||||
|
searchStateData.postValue(true) |
||||||
|
val bookSourceList = App.db.bookSourceDao().allEnabled |
||||||
|
for (item in bookSourceList) { |
||||||
|
//task取消时自动取消 by (scope = this@execute) |
||||||
|
WebBook(item).searchBook(name, scope = this@execute, context = searchPool) |
||||||
|
.timeout(30000L) |
||||||
|
.onSuccess(Dispatchers.IO) { |
||||||
|
if (it != null && it.isNotEmpty()) { |
||||||
|
val searchBook = it[0] |
||||||
|
if (searchBook.name == name && searchBook.author == author |
||||||
|
&& !searchBook.coverUrl.isNullOrEmpty() |
||||||
|
) { |
||||||
|
App.db.searchBookDao().insert(searchBook) |
||||||
|
callBack?.adapter?.let { adapter -> |
||||||
|
if (!adapter.getItems().contains(searchBook)) { |
||||||
|
withContext(Dispatchers.Main) { |
||||||
|
adapter.addItem(searchBook) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
task?.invokeOnCompletion { |
||||||
|
searchStateData.postValue(false) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCleared() { |
||||||
|
super.onCleared() |
||||||
|
searchPool.close() |
||||||
|
} |
||||||
|
|
||||||
|
interface CallBack { |
||||||
|
var adapter: CoverAdapter |
||||||
|
} |
||||||
} |
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue