Merge remote-tracking branch 'origin/desirepath' into desirepath

pull/31/head
Invinciblelee 6 years ago
commit 31d0b722e2
  1. 398
      app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt
  2. 32
      app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt
  3. 91
      app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt
  4. 16
      app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt
  5. 9
      app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt
  6. 26
      app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt
  7. 17
      app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt
  8. 13
      app/src/main/java/io/legado/app/base/adapter/animations/BaseAnimation.kt
  9. 20
      app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt
  10. 13
      app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt
  11. 14
      app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt
  12. 14
      app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt
  13. 31
      app/src/main/java/io/legado/app/data/entities/Book.kt
  14. 4
      app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt
  15. 1
      app/src/main/java/io/legado/app/data/entities/SearchBook.kt
  16. 4
      app/src/main/java/io/legado/app/data/entities/Source.kt
  17. 54
      app/src/main/java/io/legado/app/help/LayoutManager.kt
  18. 32
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  19. 5
      app/src/main/java/io/legado/app/ui/main/MainModel.kt
  20. 5
      app/src/main/java/io/legado/app/ui/search/SearchActivity.kt
  21. 34
      app/src/main/java/io/legado/app/ui/search/SearchAdapter.kt
  22. 83
      app/src/main/java/io/legado/app/ui/widget/TitleBar.kt
  23. 6
      app/src/main/java/io/legado/app/utils/StringExtensions.kt
  24. 45
      app/src/main/res/layout/activity_main.xml
  25. 4
      app/src/main/res/layout/activity_search.xml
  26. 15
      app/src/main/res/layout/app_bar_main.xml
  27. 19
      app/src/main/res/layout/item_search.xml
  28. 1
      app/src/main/res/layout/view_titlebar.xml
  29. 8
      app/src/main/res/values/attrs.xml

@ -0,0 +1,398 @@
package io.legado.app.base.adapter
import android.content.Context
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.util.*
/**
* Created by Invincible on 2017/11/24.
*
* 通用的adapter 可添加headerfooter以及不同类型item
*/
abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : RecyclerView.Adapter<ItemViewHolder>() {
constructor(context: Context, vararg delegates: Pair<Int, ItemViewDelegate<ITEM>>) : this(context) {
addItemViewDelegates(*delegates)
}
constructor(context: Context, vararg delegates: ItemViewDelegate<ITEM>) : this(context) {
addItemViewDelegates(*delegates)
}
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var headerItems: SparseArray<View>? = null
private var footerItems: SparseArray<View>? = null
private val itemDelegates: HashMap<Int, ItemViewDelegate<ITEM>> = hashMapOf()
private val items: MutableList<ITEM> = mutableListOf()
private val lock = Object()
private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null
private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null
private var itemAnimation: ItemAnimation? = null
fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) {
itemClickListener = listener
}
fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) {
itemLongClickListener = listener
}
fun bindToRecyclerView(recyclerView: RecyclerView) {
recyclerView.adapter = this
}
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegate(viewType: Int, delegate: DELEGATE) {
itemDelegates.put(viewType, delegate)
}
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegate(delegate: DELEGATE) {
itemDelegates.put(itemDelegates.size, delegate)
}
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegates(vararg delegates: DELEGATE) {
delegates.forEach {
addItemViewDelegate(it)
}
}
fun addItemViewDelegates(vararg delegates: Pair<Int, ItemViewDelegate<ITEM>>) {
delegates.forEach {
addItemViewDelegate(it.first, it.second)
}
}
fun addHeaderView(header: View) {
synchronized(lock) {
if (headerItems == null) {
headerItems = SparseArray()
}
headerItems?.let {
val index = it.size()
it.put(TYPE_HEADER_VIEW + it.size(), header)
notifyItemInserted(index)
}
}
}
fun addFooterView(footer: View) {
synchronized(lock) {
if (footerItems == null) {
footerItems = SparseArray()
}
footerItems?.let {
val index = getActualItemCount() + it.size()
it.put(TYPE_FOOTER_VIEW + it.size(), footer)
notifyItemInserted(index)
}
}
}
fun removeHeaderView(header: View) {
synchronized(lock) {
headerItems?.let {
val index = it.indexOfValue(header)
if (index >= 0) {
it.remove(index)
notifyItemRemoved(index)
}
}
}
}
fun removeFooterView(footer: View) {
synchronized(lock) {
footerItems?.let {
val index = it.indexOfValue(footer)
if (index >= 0) {
it.remove(index)
notifyItemRemoved(getActualItemCount() + index - 2)
}
}
}
}
fun setItems(items: List<ITEM>?) {
synchronized(lock) {
if (this.items.isNotEmpty()) {
this.items.clear()
}
if (items != null) {
this.items.addAll(items)
}
notifyDataSetChanged()
}
}
fun setItem(position: Int, item: ITEM) {
synchronized(lock) {
val oldSize = getActualItemCount()
if (position in 0 until oldSize) {
this.items[position] = item
notifyItemChanged(position + getHeaderCount())
}
}
}
fun addItem(item: ITEM) {
synchronized(lock) {
val oldSize = getActualItemCount()
if (this.items.add(item)) {
if (oldSize == 0) {
notifyDataSetChanged()
} else {
notifyItemInserted(oldSize + getHeaderCount())
}
}
}
}
fun addItems(position: Int, newItems: List<ITEM>) {
synchronized(lock) {
val oldSize = getActualItemCount()
if (position in 0 until oldSize) {
if (if (oldSize == 0) this.items.addAll(newItems) else this.items.addAll(position, newItems)) {
if (oldSize == 0) {
notifyDataSetChanged()
} else {
notifyItemRangeChanged(position + getHeaderCount(), newItems.size)
}
}
}
}
}
fun addItems(newItems: List<ITEM>) {
synchronized(lock) {
val oldSize = getActualItemCount()
if (this.items.addAll(newItems)) {
if (oldSize == 0) {
notifyDataSetChanged()
} else {
notifyItemRangeChanged(oldSize + getHeaderCount(), newItems.size)
}
}
}
}
fun removeItem(position: Int) {
synchronized(lock) {
if (this.items.removeAt(position) != null)
notifyItemRemoved(position + getHeaderCount())
}
}
fun removeItem(item: ITEM) {
synchronized(lock) {
if (this.items.remove(item))
notifyItemRemoved(this.items.indexOf(item) + getHeaderCount())
}
}
fun removeItems(items: List<ITEM>) {
synchronized(lock) {
if (this.items.removeAll(items))
notifyDataSetChanged()
}
}
fun swapItem(oldPosition: Int, newPosition: Int) {
synchronized(lock) {
val size = getActualItemCount()
if (oldPosition in 0 until size && newPosition in 0 until size) {
Collections.swap(this.items, oldPosition + getHeaderCount(), newPosition + getHeaderCount())
notifyDataSetChanged()
}
}
}
fun updateItem(position: Int, payload: Any) {
synchronized(lock) {
val size = getActualItemCount()
if (position in 0 until size) {
notifyItemChanged(position + getHeaderCount(), payload)
}
}
}
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) {
synchronized(lock) {
val size = getActualItemCount()
if (fromPosition in 0 until size && toPosition in 0 until size) {
notifyItemRangeChanged(fromPosition + getHeaderCount(), toPosition - fromPosition + 1, payloads)
}
}
}
fun isEmpty(): Boolean {
return items.isEmpty()
}
fun isNotEmpty(): Boolean {
return items.isNotEmpty()
}
/**
* 除去header和footer
*/
fun getActualItemCount(): Int {
return items.size
}
fun getHeaderCount(): Int {
return headerItems?.size() ?: 0
}
fun getFooterCount(): Int {
return footerItems?.size() ?: 0
}
fun getItem(position: Int): ITEM = items[position % items.size]
fun getItems(): List<ITEM> = items
protected open fun getItemViewType(item: ITEM, position: Int): Int {
return 0
}
/**
* grid 模式下使用
*/
protected open fun getSpanSize(item: ITEM, viewType: Int, position: Int): Int {
return 1
}
final override fun getItemCount(): Int {
return getActualItemCount() + getHeaderCount() + getFooterCount()
}
final override fun getItemViewType(position: Int): Int {
return when {
isHeader(position) -> TYPE_HEADER_VIEW + position
isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount()
else -> getItemViewType(getItem(getRealPosition(position)), getRealPosition(position))
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return when {
viewType < TYPE_HEADER_VIEW + getHeaderCount() -> {
ItemViewHolder(headerItems!!.get(viewType))
}
viewType >= TYPE_FOOTER_VIEW -> {
ItemViewHolder(footerItems!!.get(viewType))
}
else -> {
val holder = ItemViewHolder(inflater.inflate(itemDelegates.getValue(viewType).layoutID, parent, false))
if (itemClickListener != null) {
holder.itemView.setOnClickListener {
itemClickListener!!.invoke(holder, getItem(holder.layoutPosition))
}
}
if (itemLongClickListener != null) {
holder.itemView.setOnLongClickListener {
itemLongClickListener!!.invoke(holder, getItem(holder.layoutPosition))
}
}
holder
}
}
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
onBindViewHolder(holder, position, mutableListOf())
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList<Any>) {
if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {
itemDelegates.getValue(getItemViewType(holder.layoutPosition))
.convert(holder, getItem(holder.layoutPosition), payloads)
}
}
override fun onViewAttachedToWindow(holder: ItemViewHolder) {
super.onViewAttachedToWindow(holder)
if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {
addAnimation(holder)
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
val manager = recyclerView.layoutManager
if (manager is GridLayoutManager) {
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (isHeader(position) || isFooter(position)) manager.spanCount else getSpanSize(
getItem(position), getItemViewType(position), position
)
}
}
}
}
fun setAnimationConfig(item: ItemAnimation) {
itemAnimation = item
}
private fun isHeader(position: Int): Boolean {
return position < getHeaderCount()
}
private fun isFooter(position: Int): Boolean {
return position >= getActualItemCount() + getHeaderCount()
}
private fun getRealPosition(position: Int): Int {
return position - getHeaderCount()
}
private fun addAnimation(holder: ItemViewHolder) {
if (itemAnimation == null) {
itemAnimation = ItemAnimation.create().enabled(true)
}
itemAnimation?.let {
if (it.itemAnimEnabled) {
if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) {
startAnimation(holder, it)
it.itemAnimStartPosition = holder.layoutPosition
}
}
}
}
protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) {
for (anim in item.itemAnimation.getAnimators(holder.itemView)) {
anim.setDuration(item.itemAnimDuration).start()
anim.interpolator = item.itemAnimInterpolator
}
}
companion object {
private const val TYPE_HEADER_VIEW = Int.MIN_VALUE
private const val TYPE_FOOTER_VIEW = Int.MAX_VALUE - 999
}
}

@ -0,0 +1,32 @@
package io.legado.app.base.adapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
/**
* Created by Invincible on 2017/12/15.
*
* 上拉加载更多
*/
abstract class InfiniteScrollListener() : RecyclerView.OnScrollListener() {
private val loadMoreRunnable = Runnable { onLoadMore() }
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
// if (dy < 0 || dataLoading.isDataLoading()) return
val layoutManager:LinearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
val visibleItemCount = recyclerView.childCount
val totalItemCount = layoutManager.itemCount
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
if (totalItemCount - visibleItemCount <= firstVisibleItem + VISIBLE_THRESHOLD) {
recyclerView.post(loadMoreRunnable)
}
}
abstract fun onLoadMore()
companion object {
private const val VISIBLE_THRESHOLD = 5
}
}

@ -0,0 +1,91 @@
package io.legado.app.base.adapter
import android.view.animation.Interpolator
import android.view.animation.LinearInterpolator
import io.legado.app.base.adapter.animations.AlphaInAnimation
import io.legado.app.base.adapter.animations.BaseAnimation
import io.legado.app.base.adapter.animations.ScaleInAnimation
import io.legado.app.base.adapter.animations.SlideInBottomAnimation
import io.legado.app.base.adapter.animations.SlideInLeftAnimation
import io.legado.app.base.adapter.animations.SlideInRightAnimation
/**
* Created by Invincible on 2017/12/15.
*/
class ItemAnimation private constructor() {
var itemAnimEnabled = false
var itemAnimFirstOnly = true
var itemAnimation: BaseAnimation = SlideInBottomAnimation()
var itemAnimInterpolator: Interpolator = LinearInterpolator()
var itemAnimDuration: Long = 300L
var itemAnimStartPosition: Int = -1
fun interpolator(interpolator: Interpolator): ItemAnimation {
itemAnimInterpolator = interpolator
return this
}
fun duration(duration: Long): ItemAnimation {
itemAnimDuration = duration
return this
}
fun startPostion(startPos: Int): ItemAnimation {
itemAnimStartPosition = startPos
return this
}
fun animation(animationType: Int = FADE_IN, animation: BaseAnimation? = null): ItemAnimation {
if (animation != null) {
itemAnimation = animation
} else {
when (animationType) {
FADE_IN -> itemAnimation = AlphaInAnimation()
SCALE_IN -> itemAnimation = ScaleInAnimation()
BOTTOM_SLIDE_IN -> itemAnimation = SlideInBottomAnimation()
LEFT_SLIDE_IN -> itemAnimation = SlideInLeftAnimation()
RIGHT_SLIDE_IN -> itemAnimation = SlideInRightAnimation()
}
}
return this
}
fun enabled(enabled: Boolean): ItemAnimation {
itemAnimEnabled = enabled
return this
}
fun firstOnly(firstOnly: Boolean): ItemAnimation {
itemAnimFirstOnly = firstOnly
return this
}
companion object {
/**
* Use with [.openLoadAnimation]
*/
const val FADE_IN: Int = 0x00000001
/**
* Use with [.openLoadAnimation]
*/
const val SCALE_IN: Int = 0x00000002
/**
* Use with [.openLoadAnimation]
*/
const val BOTTOM_SLIDE_IN: Int = 0x00000003
/**
* Use with [.openLoadAnimation]
*/
const val LEFT_SLIDE_IN: Int = 0x00000004
/**
* Use with [.openLoadAnimation]
*/
const val RIGHT_SLIDE_IN: Int = 0x00000005
fun create(): ItemAnimation {
return ItemAnimation()
}
}
}

@ -0,0 +1,16 @@
package io.legado.app.base.adapter
import android.content.Context
/**
* Created by Invincible on 2017/11/24.
*
* item代理
*/
abstract class ItemViewDelegate<in ITEM>(protected val context: Context) {
abstract val layoutID: Int
abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList<Any>)
}

@ -0,0 +1,9 @@
package io.legado.app.base.adapter
import android.view.View
import androidx.recyclerview.widget.RecyclerView
/**
* Created by Invincible on 2017/11/28.
*/
class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

@ -0,0 +1,26 @@
package io.legado.app.base.adapter
import android.content.Context
/**
* Created by Invincible on 2017/12/15.
*/
abstract class SimpleRecyclerAdapter<ITEM>(context: Context) : CommonRecyclerAdapter<ITEM>(context) {
init {
addItemViewDelegate(object : ItemViewDelegate<ITEM>(context) {
override val layoutID: Int
get() = this@SimpleRecyclerAdapter.layoutID
override fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList<Any>) {
this@SimpleRecyclerAdapter.convert(holder, item, payloads)
}
})
}
abstract val layoutID: Int
abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList<Any>)
}

@ -0,0 +1,17 @@
package io.legado.app.base.adapter.animations
import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
class AlphaInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_ALPHA_FROM) : BaseAnimation {
override fun getAnimators(view: View): Array<Animator> =
arrayOf(ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f))
companion object {
private const val DEFAULT_ALPHA_FROM = 0f
}
}

@ -0,0 +1,13 @@
package io.legado.app.base.adapter.animations
import android.animation.Animator
import android.view.View
/**
* adapter item 动画
*/
interface BaseAnimation {
fun getAnimators(view: View): Array<Animator>
}

@ -0,0 +1,20 @@
package io.legado.app.base.adapter.animations
import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
class ScaleInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_SCALE_FROM) : BaseAnimation {
override fun getAnimators(view: View): Array<Animator> {
val scaleX = ObjectAnimator.ofFloat(view, "scaleX", mFrom, 1f)
val scaleY = ObjectAnimator.ofFloat(view, "scaleY", mFrom, 1f)
return arrayOf(scaleX, scaleY)
}
companion object {
private const val DEFAULT_SCALE_FROM = .5f
}
}

@ -0,0 +1,13 @@
package io.legado.app.base.adapter.animations
import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
import io.legado.app.base.adapter.animations.BaseAnimation
class SlideInBottomAnimation : BaseAnimation {
override fun getAnimators(view: View): Array<Animator> =
arrayOf(ObjectAnimator.ofFloat(view, "translationY", view.measuredHeight.toFloat(), 0f))
}

@ -0,0 +1,14 @@
package io.legado.app.base.adapter.animations
import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
import io.legado.app.base.adapter.animations.BaseAnimation
class SlideInLeftAnimation : BaseAnimation {
override fun getAnimators(view: View): Array<Animator> =
arrayOf(ObjectAnimator.ofFloat(view, "translationX", -view.rootView.width.toFloat(), 0f))
}

@ -0,0 +1,14 @@
package io.legado.app.base.adapter.animations
import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
import io.legado.app.base.adapter.animations.BaseAnimation
class SlideInRightAnimation : BaseAnimation {
override fun getAnimators(view: View): Array<Animator> =
arrayOf(ObjectAnimator.ofFloat(view, "translationX", view.rootView.width.toFloat(), 0f))
}

@ -2,29 +2,34 @@ package io.legado.app.data.entities
import android.os.Parcelable import android.os.Parcelable
import androidx.room.* import androidx.room.*
import io.legado.app.utils.strim
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@Parcelize @Parcelize
@Entity(tableName = "books", indices = [(Index(value = ["descUrl"]))]) @Entity(tableName = "books", indices = [(Index(value = ["descUrl"], unique = true))])
data class Book(@PrimaryKey data class Book(@PrimaryKey
var descUrl: String = "", // 详情页Url(本地书源存储完整文件路径) var descUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var sourceId: Int = -1, // 书源规则id(默认-1,表示本地书籍) var sourceId: Int = -1, // 书源规则id(默认-1,表示本地书籍)
var name: String = "", // 书籍名称(允许用户修改,适用于本地书籍) var name: String = "", // 书籍名称(书源获取)
var author: String? = null, // 作者名称(允许用户修改,适用于本地书籍) var customName: String? = null, // 书籍名称(用户修改)
var tag: String? = null, // 分类信息(允许用户修改,适用于本地书籍) var author: String? = null, // 作者名称(书源获取)
var coverUrl: String? = null, // 封面Url var customAuthor: String? = null, // 作者名称(用户修改)
var customCoverUrl: String? = null, // 自定义封面Url(允许用户修改,适用于网络和本地书籍) var tag: String? = null, // 分类信息(书源获取)
var description: String? = null, // 简介内容(允许用户修改,适用于网络和本地书籍) var customTag: String? = null, // 分类信息(用户修改)
var coverUrl: String? = null, // 封面Url(书源获取)
var customCoverUrl: String? = null, // 封面Url(用户修改)
var description: String? = null, // 简介内容(书源获取)
var customDescription: String? = null, // 简介内容(用户修改)
var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍) var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍)
var type: Int = 0, // 0: 文本读物, 1: 有声读物 var type: Int = 0, // 0: 文本读物, 1: 有声读物
var group: Int = 0, // 自定义分组索引号 var group: Int = 0, // 自定义分组索引号
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var latestChapterTitle: String? = null, // 最新章节标题 var latestChapterTitle: String? = null, // 最新章节标题
var latestChapterTime: Long = 0, // 最新章节标题更新时间 var latestChapterTime: Long = 0, // 最新章节标题更新时间
var lastCheckTime: Long = 0, // 最近一次更新书籍信息的时间 var lastCheckTime: Long = 0, // 最近一次更新书籍信息的时间
var lastCheckCount: Int = 0, // 最近一次发现新章节的数量 var lastCheckCount: Int = 0, // 最近一次发现新章节的数量
var totalChapterNum: Int = 0, // 书籍目录总数 var totalChapterNum: Int = 0, // 书籍目录总数
var durChapterTitle: String = "", // 当前章节名称 var durChapterTitle: String? = null, // 当前章节名称
var durChapterIndex: Int = 0, // 当前章节索引 var durChapterIndex: Int = 0, // 当前章节索引
var durChapterPos: Int = 0, // 当前阅读的进度(首行字符的索引位置) var durChapterPos: Int = 0, // 当前阅读的进度(首行字符的索引位置)
var durChapterTime: Long = 0, // 最近一次阅读书籍的时间(打开正文的时间) var durChapterTime: Long = 0, // 最近一次阅读书籍的时间(打开正文的时间)
@ -34,4 +39,12 @@ data class Book(@PrimaryKey
fun getUnreadChapterNum() = Math.max(totalChapterNum - durChapterIndex - 1, 0) fun getUnreadChapterNum() = Math.max(totalChapterNum - durChapterIndex - 1, 0)
fun getDisplayName() = customName.strim() ?: name
fun getDisplayAuthor() = customAuthor.strim() ?: author
fun getDisplayCover() = customCoverUrl.strim() ?: coverUrl
fun getDisplayDescription() = customDescription.strim() ?: description
} }

@ -16,8 +16,8 @@ data class ReplaceRule(
val pattern: String? = null, val pattern: String? = null,
val replacement: String? = null, val replacement: String? = null,
val scope: String? = null, val scope: String? = null,
val isEnabled: Boolean? = null, val isEnabled: Boolean = true,
val isRegex: Boolean? = null, val isRegex: Boolean = true,
val order: Int = 0 val order: Int = 0
) : Parcelable ) : Parcelable

@ -1,2 +1,3 @@
package io.legado.app.data.entities package io.legado.app.data.entities
class SearchBook

@ -14,14 +14,14 @@ data class Source(@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "sourceId") @ColumnInfo(name = "sourceId")
var id: Int = 0, // 编号 var id: Int = 0, // 编号
var name: String = "", // 名称 var name: String = "", // 名称
var origin: String = "", // 地址,包括 http/https var origin: String = "", // 地址,包括 http/https
var type: Int = 0, // 类型,0 文本,1 音频 var type: Int = 0, // 类型,0 文本,1 音频
var group: String? = null, // 分组 var group: String? = null, // 分组
var header: String? = null, // header var header: String? = null, // header
var loginUrl: String? = null, // 登录地址 var loginUrl: String? = null, // 登录地址
var isEnabled: Boolean = true, // 是否启用 var isEnabled: Boolean = true, // 是否启用
var lastUpdateTime: Long = 0, // 最后更新时间,用于排序 var lastUpdateTime: Long = 0, // 最后更新时间,用于排序
var serialNumber: Int = 0, // 手动排序编号 var customOrder: Int = 0, // 手动排序编号
var weight: Int = 0, // 智能排序的权重 var weight: Int = 0, // 智能排序的权重
var exploreRule: String? = null, // 发现规则 var exploreRule: String? = null, // 发现规则
var searchRule: String? = null, // 搜索规则 var searchRule: String? = null, // 搜索规则

@ -6,7 +6,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager import androidx.recyclerview.widget.StaggeredGridLayoutManager
class LayoutManager private constructor() { object LayoutManager {
interface LayoutManagerFactory { interface LayoutManagerFactory {
fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager
@ -16,50 +16,46 @@ class LayoutManager private constructor() {
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
annotation class Orientation annotation class Orientation
companion object { fun linear(): LayoutManagerFactory {
return object : LayoutManagerFactory {
override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager {
fun linear(): LayoutManagerFactory { return LinearLayoutManager(recyclerView.context)
return object : LayoutManagerFactory {
override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager {
return LinearLayoutManager(recyclerView.context)
}
} }
} }
}
fun linear(@Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory { fun linear(@Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory {
return object : LayoutManagerFactory { return object : LayoutManagerFactory {
override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager {
return LinearLayoutManager(recyclerView.context, orientation, reverseLayout) return LinearLayoutManager(recyclerView.context, orientation, reverseLayout)
}
} }
} }
}
fun grid(spanCount: Int): LayoutManagerFactory { fun grid(spanCount: Int): LayoutManagerFactory {
return object : LayoutManagerFactory { return object : LayoutManagerFactory {
override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager {
return GridLayoutManager(recyclerView.context, spanCount) return GridLayoutManager(recyclerView.context, spanCount)
}
} }
} }
}
fun grid(spanCount: Int, @Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory { fun grid(spanCount: Int, @Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory {
return object : LayoutManagerFactory { return object : LayoutManagerFactory {
override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager {
return GridLayoutManager(recyclerView.context, spanCount, orientation, reverseLayout) return GridLayoutManager(recyclerView.context, spanCount, orientation, reverseLayout)
}
} }
} }
}
fun staggeredGrid(spanCount: Int, @Orientation orientation: Int): LayoutManagerFactory { fun staggeredGrid(spanCount: Int, @Orientation orientation: Int): LayoutManagerFactory {
return object : LayoutManagerFactory { return object : LayoutManagerFactory {
override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager {
return StaggeredGridLayoutManager(spanCount, orientation) return StaggeredGridLayoutManager(spanCount, orientation)
}
} }
} }
} }

@ -5,31 +5,28 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity
import io.legado.app.ui.search.SearchActivity import io.legado.app.ui.search.SearchActivity
import io.legado.app.utils.longSnackbar
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar_main.* import kotlinx.android.synthetic.main.app_bar_main.*
import org.jetbrains.anko.startActivity import kotlinx.android.synthetic.main.view_titlebar.*
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { class MainActivity : BaseActivity<MainDataBinding, MainViewModel>(), NavigationView.OnNavigationItemSelectedListener {
override val viewModel: MainViewModel
get() = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(MainViewModel::class.java)
override fun onCreate(savedInstanceState: Bundle?) { override val layoutID: Int
super.onCreate(savedInstanceState) get() = R.layout.activity_main
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
override fun onViewModelCreated(viewModel: MainViewModel, savedInstanceState: Bundle?) {
fab.setOnClickListener { startActivity(Intent(this, SearchActivity::class.java)) } fab.setOnClickListener { startActivity(Intent(this, SearchActivity::class.java)) }
val toggle = ActionBarDrawerToggle( val toggle = ActionBarDrawerToggle(
this, drawer_layout, toolbar, this, drawer_layout, titleBar.toolbar,
R.string.navigation_drawer_open, R.string.navigation_drawer_open,
R.string.navigation_drawer_close R.string.navigation_drawer_close
) )
@ -37,7 +34,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
toggle.syncState() toggle.syncState()
nav_view.setNavigationItemSelectedListener(this) nav_view.setNavigationItemSelectedListener(this)
val mainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
} }
override fun onBackPressed() { override fun onBackPressed() {
@ -55,14 +51,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
return true return true
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will return super.onCompatOptionsItemSelected(item)
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
} }
override fun onNavigationItemSelected(item: MenuItem): Boolean { override fun onNavigationItemSelected(item: MenuItem): Boolean {

@ -1,5 +0,0 @@
package io.legado.app.ui.main
class MainModel {
}

@ -4,7 +4,7 @@ import android.os.Bundle
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
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.search.SearchDataBinding import io.legado.app.help.LayoutManager
class SearchActivity : BaseActivity<SearchDataBinding, SearchViewModel>() { class SearchActivity : BaseActivity<SearchDataBinding, SearchViewModel>() {
@ -15,9 +15,6 @@ class SearchActivity : BaseActivity<SearchDataBinding, SearchViewModel>() {
get() = R.layout.activity_search get() = R.layout.activity_search
override fun onViewModelCreated(viewModel: SearchViewModel, savedInstanceState: Bundle?) { override fun onViewModelCreated(viewModel: SearchViewModel, savedInstanceState: Bundle?) {
dataBinding.searchViewModel = viewModel
} }
} }

@ -0,0 +1,34 @@
package io.legado.app.ui.search
import android.content.Context
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewDelegate
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.SearchBook
import kotlinx.android.synthetic.main.item_search.view.*
class SearchAdapter(context: Context) : SimpleRecyclerAdapter<SearchBook>(context) {
init {
addItemViewDelegate(TestItemDelegate(context))
}
override val layoutID: Int
get() = R.layout.item_search
override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList<Any>) {
holder.itemView.bookName.text = "我欲封天"
}
internal class TestItemDelegate(context: Context) : ItemViewDelegate<SearchBook>(context){
override val layoutID: Int
get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList<Any>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
}

@ -5,7 +5,12 @@ import android.content.res.ColorStateList
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Menu
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import io.legado.app.R import io.legado.app.R
@ -13,11 +18,16 @@ import kotlinx.android.synthetic.main.view_titlebar.view.*
class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, attrs) { class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, attrs) {
val toolbar: Toolbar
val menu: Menu
get() = toolbar.menu
init { init {
inflate(context, R.layout.view_titlebar, this) inflate(context, R.layout.view_titlebar, this)
toolbar = findViewById(R.id.toolbar)
val a = context.obtainStyledAttributes( val a = context.obtainStyledAttributes(
attrs, R.styleable.TitleBar, attrs, R.styleable.TitleBar,
0, 0 R.attr.titleBarStyle, 0
) )
val navigationIcon = a.getDrawable(R.styleable.TitleBar_navigationIcon) val navigationIcon = a.getDrawable(R.styleable.TitleBar_navigationIcon)
val navigationContentDescription = a.getText(R.styleable.TitleBar_navigationContentDescription) val navigationContentDescription = a.getText(R.styleable.TitleBar_navigationContentDescription)
@ -25,28 +35,89 @@ class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, a
val navigationIconTintMode = a.getInt(R.styleable.TitleBar_navigationIconTintMode, 9) val navigationIconTintMode = a.getInt(R.styleable.TitleBar_navigationIconTintMode, 9)
val showNavigationIcon = a.getBoolean(R.styleable.TitleBar_showNavigationIcon, true) val showNavigationIcon = a.getBoolean(R.styleable.TitleBar_showNavigationIcon, true)
val attachToActivity = a.getBoolean(R.styleable.TitleBar_attachToActivity, true) val attachToActivity = a.getBoolean(R.styleable.TitleBar_attachToActivity, true)
val titleText = a.getString(R.styleable.TitleBar_title)
val subtitleText = a.getString(R.styleable.TitleBar_subtitle)
a.recycle() a.recycle()
if (showNavigationIcon) { toolbar.apply {
toolbar.apply { if(showNavigationIcon){
this.navigationIcon = navigationIcon this.navigationIcon = navigationIcon
this.navigationContentDescription = navigationContentDescription this.navigationContentDescription = navigationContentDescription
wrapDrawableTint(this.navigationIcon, navigationIconTint, navigationIconTintMode) wrapDrawableTint(this.navigationIcon, navigationIconTint, navigationIconTintMode)
} }
if (a.hasValue(R.styleable.TitleBar_titleTextAppearance)) {
this.setTitleTextAppearance(context, a.getResourceId(R.styleable.TitleBar_titleTextAppearance, 0))
}
if (a.hasValue(R.styleable.TitleBar_titleTextColor)) {
this.setTitleTextColor(a.getColor(R.styleable.TitleBar_titleTextColor, -0x1))
}
if (a.hasValue(R.styleable.TitleBar_subtitleTextAppearance)) {
this.setSubtitleTextAppearance(context, a.getResourceId(R.styleable.TitleBar_subtitleTextAppearance, 0))
}
if (a.hasValue(R.styleable.TitleBar_subtitleTextColor)) {
this.setSubtitleTextColor(a.getColor(R.styleable.TitleBar_subtitleTextColor, -0x1))
}
if(!titleText.isNullOrBlank()){
this.title = titleText
}
if(!subtitleText.isNullOrBlank()){
this.subtitle = subtitleText
}
} }
if (attachToActivity) { if (attachToActivity) {
attachToActivity(context) attachToActivity(context)
} }
} }
fun setNavigationOnClickListener(clickListener: ((View) -> Unit)){
toolbar.setNavigationOnClickListener(clickListener)
}
fun setTitle(title: CharSequence?) {
toolbar.title = title
}
fun setTitle(titleId: Int) {
toolbar.setTitle(titleId)
}
fun setSubTitle(subtitle: CharSequence?) {
toolbar.subtitle = subtitle
}
fun setSubTitle(subtitleId: Int) {
toolbar.setSubtitle(subtitleId)
}
fun setTitleTextColor(@ColorInt color: Int){
toolbar.setTitleTextColor(color)
}
fun setTitleTextAppearance(@StyleRes resId: Int){
toolbar.setTitleTextAppearance(context, resId)
}
fun setSubTitleTextColor(@ColorInt color: Int){
toolbar.setSubtitleTextColor(color)
}
fun setSubTitleTextAppearance(@StyleRes resId: Int){
toolbar.setSubtitleTextAppearance(context, resId)
}
private fun attachToActivity(context: Context) { private fun attachToActivity(context: Context) {
val activity = getCompatActivity(context) val activity = getCompatActivity(context)
activity?.let { activity?.let {
activity.setSupportActionBar(toolbar) activity.setSupportActionBar(toolbar)
activity.supportActionBar?.let { activity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
it.setDisplayHomeAsUpEnabled(true)
}
} }
} }

@ -0,0 +1,6 @@
package io.legado.app.utils
fun String?.strim() = if (this.isNullOrBlank()) null else this.trim()
fun String.isAbsUrl() = this.startsWith("http://", true)
|| this.startsWith("https://", true)

@ -1,26 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
xmlns:app="http://schemas.android.com/apk/res-auto" <data class=".ui.main.MainDataBinding">
xmlns:tools="http://schemas.android.com/tools" <variable name="MainViewModel" type="io.legado.app.ui.main.MainViewModel"/>
android:id="@+id/drawer_layout" </data>
android:layout_width="match_parent" <androidx.drawerlayout.widget.DrawerLayout
android:layout_height="match_parent" android:id="@+id/drawer_layout"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<include
layout="@layout/app_bar_main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main" tools:openDrawer="start">
app:menu="@menu/activity_main_drawer"/>
<include
layout="@layout/app_bar_main"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer"/>
</androidx.drawerlayout.widget.DrawerLayout> </androidx.drawerlayout.widget.DrawerLayout>
</layout>

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"> <layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data class=".search.SearchDataBinding"> <data class=".ui.search.SearchDataBinding">
<variable name="SearchViewModel" type="io.legado.app.ui.search.SearchViewModel"/> <variable name="SearchViewModel" type="io.legado.app.ui.search.SearchViewModel"/>
</data> </data>
@ -17,7 +17,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
/> app:title="搜索"/>
<io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout <io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"

@ -7,19 +7,10 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".ui.main.MainActivity"> tools:context=".ui.main.MainActivity">
<com.google.android.material.appbar.AppBarLayout <io.legado.app.ui.widget.TitleBar
android:layout_height="wrap_content" android:id="@+id/titleBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:theme="@style/AppTheme.AppBarOverlay"> android:layout_height="wrap_content"/>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_main"/> <include layout="@layout/content_main"/>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/bookName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:text="测试"/>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -5,4 +5,5 @@
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay"
app:popupTheme="@style/AppTheme.PopupOverlay"/> app:popupTheme="@style/AppTheme.PopupOverlay"/>

@ -2,6 +2,12 @@
<resources> <resources>
<declare-styleable name="TitleBar"> <declare-styleable name="TitleBar">
<attr name="title" />
<attr name="subtitle" />
<attr name="titleTextAppearance" />
<attr name="titleTextColor" />
<attr name="subtitleTextAppearance" />
<attr name="subtitleTextColor" />
<attr name="attachToActivity" format="boolean" /> <attr name="attachToActivity" format="boolean" />
<attr name="showNavigationIcon" format="boolean" /> <attr name="showNavigationIcon" format="boolean" />
<attr name="navigationIcon" format="reference" /> <attr name="navigationIcon" format="reference" />
@ -29,6 +35,8 @@
</attr> </attr>
</declare-styleable> </declare-styleable>
<attr name="titleBarStyle" format="reference" />
<declare-styleable name="DynamicFrameLayout"> <declare-styleable name="DynamicFrameLayout">
<attr name="errorSrc" format="reference" /> <attr name="errorSrc" format="reference" />
<attr name="emptySrc" format="reference" /> <attr name="emptySrc" format="reference" />

Loading…
Cancel
Save