diff --git a/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt new file mode 100644 index 000000000..c9b9f0cf2 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt @@ -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 可添加header,footer,以及不同类型item + */ +abstract class CommonRecyclerAdapter(protected val context: Context) : RecyclerView.Adapter() { + + constructor(context: Context, vararg delegates: Pair>) : this(context) { + addItemViewDelegates(*delegates) + } + + constructor(context: Context, vararg delegates: ItemViewDelegate) : this(context) { + addItemViewDelegates(*delegates) + } + + private val inflater: LayoutInflater = LayoutInflater.from(context) + + private var headerItems: SparseArray? = null + private var footerItems: SparseArray? = null + + private val itemDelegates: HashMap> = hashMapOf() + private val items: MutableList = 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 > addItemViewDelegate(viewType: Int, delegate: DELEGATE) { + itemDelegates.put(viewType, delegate) + } + + fun > addItemViewDelegate(delegate: DELEGATE) { + itemDelegates.put(itemDelegates.size, delegate) + } + + fun > addItemViewDelegates(vararg delegates: DELEGATE) { + delegates.forEach { + addItemViewDelegate(it) + } + } + + fun addItemViewDelegates(vararg delegates: Pair>) { + 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?) { + 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) { + 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) { + 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) { + 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 = 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) { + 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 + } + +} + + + + diff --git a/app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt b/app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt new file mode 100644 index 000000000..fc7c48bab --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt @@ -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 + } +} diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt new file mode 100644 index 000000000..6db4a7415 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt b/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt new file mode 100644 index 000000000..789ca250e --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt @@ -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(protected val context: Context) { + + abstract val layoutID: Int + + abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt b/app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt new file mode 100644 index 000000000..c415fa4b6 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt @@ -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) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt new file mode 100644 index 000000000..176098f50 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt @@ -0,0 +1,26 @@ +package io.legado.app.base.adapter + +import android.content.Context + +/** + * Created by Invincible on 2017/12/15. + */ +abstract class SimpleRecyclerAdapter(context: Context) : CommonRecyclerAdapter(context) { + + init { + addItemViewDelegate(object : ItemViewDelegate(context) { + override val layoutID: Int + get() = this@SimpleRecyclerAdapter.layoutID + + override fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList) { + this@SimpleRecyclerAdapter.convert(holder, item, payloads) + } + + }) + + } + + abstract val layoutID: Int + + abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt new file mode 100644 index 000000000..b307300a3 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt @@ -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 = + arrayOf(ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f)) + + companion object { + + private const val DEFAULT_ALPHA_FROM = 0f + } +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/BaseAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/BaseAnimation.kt new file mode 100644 index 000000000..735ceca57 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/BaseAnimation.kt @@ -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 + +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt new file mode 100644 index 000000000..3464f4079 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt @@ -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 { + 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 + } +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt new file mode 100644 index 000000000..0fd2bfde6 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt @@ -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 = + arrayOf(ObjectAnimator.ofFloat(view, "translationY", view.measuredHeight.toFloat(), 0f)) +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt new file mode 100644 index 000000000..daa6f17a1 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt @@ -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 = + arrayOf(ObjectAnimator.ofFloat(view, "translationX", -view.rootView.width.toFloat(), 0f)) +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt new file mode 100644 index 000000000..71c8feac7 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt @@ -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 = + arrayOf(ObjectAnimator.ofFloat(view, "translationX", view.rootView.width.toFloat(), 0f)) +} diff --git a/app/src/main/java/io/legado/app/data/entities/Book.kt b/app/src/main/java/io/legado/app/data/entities/Book.kt index e207f8c03..fc601f9fe 100644 --- a/app/src/main/java/io/legado/app/data/entities/Book.kt +++ b/app/src/main/java/io/legado/app/data/entities/Book.kt @@ -2,29 +2,34 @@ package io.legado.app.data.entities import android.os.Parcelable import androidx.room.* +import io.legado.app.utils.strim import kotlinx.android.parcel.Parcelize @Parcelize -@Entity(tableName = "books", indices = [(Index(value = ["descUrl"]))]) +@Entity(tableName = "books", indices = [(Index(value = ["descUrl"], unique = true))]) data class Book(@PrimaryKey var descUrl: String = "", // 详情页Url(本地书源存储完整文件路径) + var tocUrl: String = "", // 目录页Url (toc=table of Contents) var sourceId: Int = -1, // 书源规则id(默认-1,表示本地书籍) - var name: String = "", // 书籍名称(允许用户修改,适用于本地书籍) - var author: String? = null, // 作者名称(允许用户修改,适用于本地书籍) - var tag: String? = null, // 分类信息(允许用户修改,适用于本地书籍) - var coverUrl: String? = null, // 封面Url - var customCoverUrl: String? = null, // 自定义封面Url(允许用户修改,适用于网络和本地书籍) - var description: String? = null, // 简介内容(允许用户修改,适用于网络和本地书籍) + var name: String = "", // 书籍名称(书源获取) + var customName: String? = null, // 书籍名称(用户修改) + var author: String? = null, // 作者名称(书源获取) + var customAuthor: String? = null, // 作者名称(用户修改) + var tag: 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 type: Int = 0, // 0: 文本读物, 1: 有声读物 var group: Int = 0, // 自定义分组索引号 - var tocUrl: String = "", // 目录页Url (toc=table of Contents) var latestChapterTitle: String? = null, // 最新章节标题 var latestChapterTime: Long = 0, // 最新章节标题更新时间 var lastCheckTime: Long = 0, // 最近一次更新书籍信息的时间 var lastCheckCount: Int = 0, // 最近一次发现新章节的数量 var totalChapterNum: Int = 0, // 书籍目录总数 - var durChapterTitle: String = "", // 当前章节名称 + var durChapterTitle: String? = null, // 当前章节名称 var durChapterIndex: Int = 0, // 当前章节索引 var durChapterPos: Int = 0, // 当前阅读的进度(首行字符的索引位置) var durChapterTime: Long = 0, // 最近一次阅读书籍的时间(打开正文的时间) @@ -34,4 +39,12 @@ data class Book(@PrimaryKey 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 + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt b/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt index fdfa41d3f..2436fb641 100644 --- a/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt @@ -16,8 +16,8 @@ data class ReplaceRule( val pattern: String? = null, val replacement: String? = null, val scope: String? = null, - val isEnabled: Boolean? = null, - val isRegex: Boolean? = null, + val isEnabled: Boolean = true, + val isRegex: Boolean = true, val order: Int = 0 ) : Parcelable diff --git a/app/src/main/java/io/legado/app/data/entities/SearchBook.kt b/app/src/main/java/io/legado/app/data/entities/SearchBook.kt index feecd7295..8994f14bd 100644 --- a/app/src/main/java/io/legado/app/data/entities/SearchBook.kt +++ b/app/src/main/java/io/legado/app/data/entities/SearchBook.kt @@ -1,2 +1,3 @@ package io.legado.app.data.entities +class SearchBook \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/Source.kt b/app/src/main/java/io/legado/app/data/entities/Source.kt index 1c6cfcf18..aa38236b5 100644 --- a/app/src/main/java/io/legado/app/data/entities/Source.kt +++ b/app/src/main/java/io/legado/app/data/entities/Source.kt @@ -14,14 +14,14 @@ data class Source(@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "sourceId") var id: Int = 0, // 编号 var name: String = "", // 名称 - var origin: String = "", // 地址,包括 http/https + var origin: String = "", // 地址,包括 http/https var type: Int = 0, // 类型,0 文本,1 音频 var group: String? = null, // 分组 var header: String? = null, // header var loginUrl: String? = null, // 登录地址 var isEnabled: Boolean = true, // 是否启用 var lastUpdateTime: Long = 0, // 最后更新时间,用于排序 - var serialNumber: Int = 0, // 手动排序编号 + var customOrder: Int = 0, // 手动排序编号 var weight: Int = 0, // 智能排序的权重 var exploreRule: String? = null, // 发现规则 var searchRule: String? = null, // 搜索规则 diff --git a/app/src/main/java/io/legado/app/help/LayoutManager.kt b/app/src/main/java/io/legado/app/help/LayoutManager.kt index 9fc551222..e633993c7 100644 --- a/app/src/main/java/io/legado/app/help/LayoutManager.kt +++ b/app/src/main/java/io/legado/app/help/LayoutManager.kt @@ -6,7 +6,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.StaggeredGridLayoutManager -class LayoutManager private constructor() { +object LayoutManager { interface LayoutManagerFactory { fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager @@ -16,50 +16,46 @@ class LayoutManager private constructor() { @Retention(AnnotationRetention.SOURCE) annotation class Orientation - companion object { - - - fun linear(): LayoutManagerFactory { - return object : LayoutManagerFactory { - override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { - return LinearLayoutManager(recyclerView.context) - } + fun linear(): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return LinearLayoutManager(recyclerView.context) } } + } - fun linear(@Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory { - return object : LayoutManagerFactory { - override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { - return LinearLayoutManager(recyclerView.context, orientation, reverseLayout) - } + fun linear(@Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return LinearLayoutManager(recyclerView.context, orientation, reverseLayout) } } + } - fun grid(spanCount: Int): LayoutManagerFactory { - return object : LayoutManagerFactory { - override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { - return GridLayoutManager(recyclerView.context, spanCount) - } + fun grid(spanCount: Int): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return GridLayoutManager(recyclerView.context, spanCount) } } + } - fun grid(spanCount: Int, @Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory { - return object : LayoutManagerFactory { - override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { - return GridLayoutManager(recyclerView.context, spanCount, orientation, reverseLayout) - } + fun grid(spanCount: Int, @Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return GridLayoutManager(recyclerView.context, spanCount, orientation, reverseLayout) } } + } - fun staggeredGrid(spanCount: Int, @Orientation orientation: Int): LayoutManagerFactory { - return object : LayoutManagerFactory { - override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { - return StaggeredGridLayoutManager(spanCount, orientation) - } + fun staggeredGrid(spanCount: Int, @Orientation orientation: Int): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return StaggeredGridLayoutManager(spanCount, orientation) } } } diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index 47a023576..cf4a2329d 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -5,31 +5,28 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.ActionBarDrawerToggle -import androidx.appcompat.app.AppCompatActivity import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProviders import com.google.android.material.navigation.NavigationView import io.legado.app.R +import io.legado.app.base.BaseActivity 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.app_bar_main.* -import org.jetbrains.anko.startActivity +import kotlinx.android.synthetic.main.view_titlebar.* -class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { - - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - setSupportActionBar(toolbar) +class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { + override val viewModel: MainViewModel + get() = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(MainViewModel::class.java) + override val layoutID: Int + get() = R.layout.activity_main + override fun onViewModelCreated(viewModel: MainViewModel, savedInstanceState: Bundle?) { fab.setOnClickListener { startActivity(Intent(this, SearchActivity::class.java)) } val toggle = ActionBarDrawerToggle( - this, drawer_layout, toolbar, + this, drawer_layout, titleBar.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close ) @@ -37,7 +34,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte toggle.syncState() nav_view.setNavigationItemSelectedListener(this) - val mainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) } override fun onBackPressed() { @@ -55,14 +51,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte return true } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - // Handle action bar item clicks here. The action bar will - // 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 onCompatOptionsItemSelected(item: MenuItem): Boolean { + return super.onCompatOptionsItemSelected(item) } override fun onNavigationItemSelected(item: MenuItem): Boolean { diff --git a/app/src/main/java/io/legado/app/ui/main/MainModel.kt b/app/src/main/java/io/legado/app/ui/main/MainModel.kt deleted file mode 100644 index 86da567f6..000000000 --- a/app/src/main/java/io/legado/app/ui/main/MainModel.kt +++ /dev/null @@ -1,5 +0,0 @@ -package io.legado.app.ui.main - -class MainModel { - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt b/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt index f583abc6f..14994c535 100644 --- a/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt +++ b/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt @@ -4,7 +4,7 @@ import android.os.Bundle import androidx.lifecycle.ViewModelProvider import io.legado.app.R import io.legado.app.base.BaseActivity -import io.legado.app.search.SearchDataBinding +import io.legado.app.help.LayoutManager class SearchActivity : BaseActivity() { @@ -15,9 +15,6 @@ class SearchActivity : BaseActivity() { get() = R.layout.activity_search override fun onViewModelCreated(viewModel: SearchViewModel, savedInstanceState: Bundle?) { - dataBinding.searchViewModel = viewModel - - } } diff --git a/app/src/main/java/io/legado/app/ui/search/SearchAdapter.kt b/app/src/main/java/io/legado/app/ui/search/SearchAdapter.kt new file mode 100644 index 000000000..a23ee7605 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/search/SearchAdapter.kt @@ -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(context) { + + init { + addItemViewDelegate(TestItemDelegate(context)) + } + + override val layoutID: Int + get() = R.layout.item_search + + override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList) { + holder.itemView.bookName.text = "我欲封天" + } + + internal class TestItemDelegate(context: Context) : ItemViewDelegate(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) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt index e7df5b914..548c22ec5 100644 --- a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt @@ -5,7 +5,12 @@ import android.content.res.ColorStateList import android.graphics.PorterDuff import android.graphics.drawable.Drawable 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.widget.Toolbar import androidx.core.graphics.drawable.DrawableCompat import com.google.android.material.appbar.AppBarLayout 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) { + val toolbar: Toolbar + val menu: Menu + get() = toolbar.menu + init { inflate(context, R.layout.view_titlebar, this) + toolbar = findViewById(R.id.toolbar) val a = context.obtainStyledAttributes( attrs, R.styleable.TitleBar, - 0, 0 + R.attr.titleBarStyle, 0 ) val navigationIcon = a.getDrawable(R.styleable.TitleBar_navigationIcon) 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 showNavigationIcon = a.getBoolean(R.styleable.TitleBar_showNavigationIcon, 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() - if (showNavigationIcon) { - toolbar.apply { + toolbar.apply { + if(showNavigationIcon){ this.navigationIcon = navigationIcon this.navigationContentDescription = navigationContentDescription 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) { 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) { val activity = getCompatActivity(context) activity?.let { activity.setSupportActionBar(toolbar) - activity.supportActionBar?.let { - it.setDisplayHomeAsUpEnabled(true) - } + activity.supportActionBar?.setDisplayHomeAsUpEnabled(true) } } diff --git a/app/src/main/java/io/legado/app/utils/StringExtensions.kt b/app/src/main/java/io/legado/app/utils/StringExtensions.kt new file mode 100644 index 000000000..23f35a357 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/StringExtensions.kt @@ -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) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ac9a123b3..92789a7fb 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,26 +1,29 @@ - - - + + + + - - + tools:openDrawer="start"> + + + + - + + diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml index 486b05137..da4d4a9c6 100644 --- a/app/src/main/res/layout/activity_search.xml +++ b/app/src/main/res/layout/activity_search.xml @@ -1,7 +1,7 @@ - + @@ -17,7 +17,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" - /> + app:title="搜索"/> - - - - - + android:layout_height="wrap_content"/> diff --git a/app/src/main/res/layout/item_search.xml b/app/src/main/res/layout/item_search.xml new file mode 100644 index 000000000..4fe3215d2 --- /dev/null +++ b/app/src/main/res/layout/item_search.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_titlebar.xml b/app/src/main/res/layout/view_titlebar.xml index f1befb3b3..82f286582 100644 --- a/app/src/main/res/layout/view_titlebar.xml +++ b/app/src/main/res/layout/view_titlebar.xml @@ -5,4 +5,5 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" + android:theme="@style/AppTheme.AppBarOverlay" app:popupTheme="@style/AppTheme.PopupOverlay"/> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 7456589c4..fc861ecca 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -2,6 +2,12 @@ + + + + + + @@ -29,6 +35,8 @@ + +