diff --git a/app/build.gradle b/app/build.gradle
index 1c6c7a570..966ba8516 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -41,6 +41,12 @@ kapt {
}
}
+kotlin{
+ experimental{
+ coroutines "enable"
+ }
+}
+
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
@@ -68,6 +74,16 @@ dependencies {
implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version"
//协程
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
+ //规则相关
+ implementation 'pub.devrel:easypermissions:3.0.0'
+ implementation 'com.google.code.gson:gson:2.8.5'
+ implementation 'com.jayway.jsonpath:json-path:2.4.0'
+ implementation 'org.jsoup:jsoup:1.12.1'
+
+ //Retrofit
+ implementation 'com.squareup.okhttp3:logging-interceptor:3.14.0'//
+ implementation 'com.squareup.retrofit2:retrofit:2.5.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 10ee7aea6..7c5e32f4a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -39,6 +39,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/App.kt b/app/src/main/java/io/legado/app/App.kt
index d94919f22..6ef0e877d 100644
--- a/app/src/main/java/io/legado/app/App.kt
+++ b/app/src/main/java/io/legado/app/App.kt
@@ -1,6 +1,7 @@
package io.legado.app
import android.app.Application
+import io.legado.app.data.AppDatabase
class App : Application() {
@@ -8,10 +9,15 @@ class App : Application() {
@JvmStatic
lateinit var INSTANCE: App
private set
+
+ @JvmStatic
+ lateinit var db: AppDatabase
+ private set
}
override fun onCreate() {
super.onCreate()
INSTANCE = this
+ db = AppDatabase.createDatabase(INSTANCE)
}
}
diff --git a/app/src/main/java/io/legado/app/base/BaseViewModel.kt b/app/src/main/java/io/legado/app/base/BaseViewModel.kt
new file mode 100644
index 000000000..74bc4bfc2
--- /dev/null
+++ b/app/src/main/java/io/legado/app/base/BaseViewModel.kt
@@ -0,0 +1,52 @@
+package io.legado.app.base
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import kotlinx.coroutines.*
+import kotlin.coroutines.CoroutineContext
+
+open class BaseViewModel(application: Application) : AndroidViewModel(application), CoroutineScope {
+ override val coroutineContext: CoroutineContext
+ get() = Dispatchers.Main
+
+
+ private val launchManager: MutableList = mutableListOf()
+
+ protected fun launchOnUI(
+ tryBlock: suspend CoroutineScope.() -> Unit,//成功
+ errorBlock: suspend CoroutineScope.(Throwable) -> Unit,//失败
+ finallyBlock: suspend CoroutineScope.() -> Unit//结束
+ ) {
+ launchOnUI {
+ tryCatch(tryBlock, errorBlock, finallyBlock)
+ }
+ }
+
+ /**
+ * add launch task to [launchManager]
+ */
+ private fun launchOnUI(block: suspend CoroutineScope.() -> Unit) {
+ val job = launch { block() }//主线程
+ launchManager.add(job)
+ job.invokeOnCompletion { launchManager.remove(job) }
+ }
+
+ private suspend fun tryCatch(
+ tryBlock: suspend CoroutineScope.() -> Unit,
+ errorBlock: suspend CoroutineScope.(Throwable) -> Unit,
+ finallyBlock: suspend CoroutineScope.() -> Unit
+ ) {
+ try {
+ coroutineScope { tryBlock() }
+ } catch (e: Throwable) {
+ coroutineScope { errorBlock(e) }
+ } finally {
+ coroutineScope { finallyBlock() }
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ launchManager.clear()
+ }
+}
\ No newline at end of file
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..ce7b3d4a6
--- /dev/null
+++ b/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt
@@ -0,0 +1,405 @@
+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? = if (position in 0 until items.size) items[position] else null
+
+ 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 -> getItem(getActualPosition(position))?.let { getItemViewType(it, getActualPosition(position)) } ?: 0
+ }
+ }
+
+ 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 {
+ getItem(holder.layoutPosition)?.let {
+ itemClickListener?.invoke(holder, it)
+ }
+ }
+ }
+
+ if (itemLongClickListener != null) {
+ holder.itemView.setOnLongClickListener {
+ getItem(holder.layoutPosition)?.let {
+ itemLongClickListener?.invoke(holder, it) ?: true
+ } ?: true
+ }
+ }
+
+ holder
+ }
+ }
+ }
+
+
+ final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
+ }
+
+ final override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList) {
+ if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {
+ getItem(holder.layoutPosition)?.let {
+ itemDelegates.getValue(getItemViewType(holder.layoutPosition))
+ .convert(holder, it, 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 getItem(position)?.let {
+ if (isHeader(position) || isFooter(position)) manager.spanCount else getSpanSize(
+ it, getItemViewType(position), position
+ )
+ } ?: manager.spanCount
+ }
+ }
+ }
+ }
+
+ fun setItemAnimation(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 getActualPosition(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/constant/AppConst.kt b/app/src/main/java/io/legado/app/constant/AppConst.kt
index 6f9e7b6b9..89a1d204f 100644
--- a/app/src/main/java/io/legado/app/constant/AppConst.kt
+++ b/app/src/main/java/io/legado/app/constant/AppConst.kt
@@ -4,4 +4,8 @@ object AppConst {
const val channelIdDownload = "channel_download"
const val channelIdReadAloud = "channel_read_aloud"
const val channelIdWeb = "channel_web"
+
+ const val APP_TAG = "Legado"
+ const val RC_IMPORT_YUEDU_DATA = 100
+
}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/AppDatabase.kt b/app/src/main/java/io/legado/app/data/AppDatabase.kt
index 327651493..f5db6c908 100644
--- a/app/src/main/java/io/legado/app/data/AppDatabase.kt
+++ b/app/src/main/java/io/legado/app/data/AppDatabase.kt
@@ -7,6 +7,7 @@ import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.legado.app.data.dao.BookDao
+import io.legado.app.data.dao.ReplaceRuleDao
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.Chapter
import io.legado.app.data.entities.ReplaceRule
@@ -51,5 +52,6 @@ abstract class AppDatabase : RoomDatabase() {
}
abstract fun bookDao(): BookDao
+ abstract fun replaceRuleDao(): ReplaceRuleDao
}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/api/CommonHttpApi.kt b/app/src/main/java/io/legado/app/data/api/CommonHttpApi.kt
new file mode 100644
index 000000000..08e06f52f
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/api/CommonHttpApi.kt
@@ -0,0 +1,14 @@
+package io.legado.app.data.api
+
+import kotlinx.coroutines.Deferred
+import retrofit2.http.*
+
+interface CommonHttpApi {
+
+ @GET
+ fun get(@Url url: String, @QueryMap map: Map): Deferred
+
+ @FormUrlEncoded
+ @POST
+ fun post(@Url url: String, @FieldMap map: Map): Deferred
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt
new file mode 100644
index 000000000..9b92e4c6a
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt
@@ -0,0 +1,49 @@
+package io.legado.app.data.dao
+
+import androidx.lifecycle.LiveData
+import androidx.paging.DataSource
+import androidx.room.*
+import io.legado.app.data.entities.ReplaceRule
+
+
+@Dao
+interface ReplaceRuleDao {
+
+ @Query("SELECT * FROM replace_rules ORDER BY sortOrder ASC")
+ fun observeAll(): DataSource.Factory
+
+ @Query("SELECT id FROM replace_rules ORDER BY sortOrder ASC")
+ fun observeAllIds(): LiveData
>
+
+ @get:Query("SELECT MAX(sortOrder) FROM replace_rules")
+ val maxOrder: Int
+
+ @get:Query("SELECT * FROM replace_rules ORDER BY sortOrder ASC")
+ val all: List
+
+ @get:Query("SELECT * FROM replace_rules WHERE isEnabled = 1 ORDER BY sortOrder ASC")
+ val allEnabled: List
+
+ @Query("SELECT * FROM replace_rules WHERE id = :id")
+ fun findById(id: Int): ReplaceRule?
+
+ @Query("SELECT * FROM replace_rules WHERE id in (:ids)")
+ fun findByIds(vararg ids: Int): List
+
+ @Query("SELECT * FROM replace_rules WHERE isEnabled = 1 AND scope LIKE '%' || :scope || '%'")
+ fun findEnabledByScope(scope: String): List
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(vararg replaceRules: ReplaceRule)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(replaceRule: ReplaceRule): Long
+
+ @Update
+ fun update(vararg replaceRules: ReplaceRule)
+
+ @Delete
+ fun delete(vararg replaceRules: ReplaceRule)
+
+
+}
\ 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 2436fb641..8b5a0fda6 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
@@ -1,6 +1,7 @@
package io.legado.app.data.entities
import android.os.Parcelable
+import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@@ -11,14 +12,15 @@ import kotlinx.android.parcel.Parcelize
indices = [(Index(value = ["id"]))])
data class ReplaceRule(
@PrimaryKey(autoGenerate = true)
- val id: Int = 0,
- val summary: String? = null,
- val pattern: String? = null,
- val replacement: String? = null,
- val scope: String? = null,
- val isEnabled: Boolean = true,
- val isRegex: Boolean = true,
- val order: Int = 0
+ var id: Int = 0,
+ var name: String? = null,
+ var pattern: String? = null,
+ var replacement: String? = null,
+ var scope: String? = null,
+ var isEnabled: Boolean = true,
+ var isRegex: Boolean = true,
+ @ColumnInfo(name = "sortOrder")
+ var 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/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/help/http/CoroutinesCallAdapterFactory.kt b/app/src/main/java/io/legado/app/help/http/CoroutinesCallAdapterFactory.kt
new file mode 100644
index 000000000..bf296f5bb
--- /dev/null
+++ b/app/src/main/java/io/legado/app/help/http/CoroutinesCallAdapterFactory.kt
@@ -0,0 +1,107 @@
+package io.legado.app.help.http
+
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import retrofit2.*
+import java.lang.reflect.ParameterizedType
+import java.lang.reflect.Type
+
+class CoroutinesCallAdapterFactory private constructor() : CallAdapter.Factory() {
+ companion object {
+ @JvmStatic @JvmName("create")
+ operator fun invoke() = CoroutinesCallAdapterFactory()
+ }
+
+ override fun get(
+ returnType: Type,
+ annotations: Array,
+ retrofit: Retrofit
+ ): CallAdapter<*, *>? {
+ if (Deferred::class.java != getRawType(returnType)) {
+ return null
+ }
+ if (returnType !is ParameterizedType) {
+ throw IllegalStateException(
+ "Deferred return type must be parameterized as Deferred or Deferred")
+ }
+ val responseType = getParameterUpperBound(0, returnType)
+
+ val rawDeferredType = getRawType(responseType)
+ return if (rawDeferredType == Response::class.java) {
+ if (responseType !is ParameterizedType) {
+ throw IllegalStateException(
+ "Response must be parameterized as Response or Response")
+ }
+ ResponseCallAdapter(
+ getParameterUpperBound(
+ 0,
+ responseType
+ )
+ )
+ } else {
+ BodyCallAdapter(responseType)
+ }
+ }
+
+ private class BodyCallAdapter(
+ private val responseType: Type
+ ) : CallAdapter> {
+
+ override fun responseType() = responseType
+
+ override fun adapt(call: Call): Deferred {
+ val deferred = CompletableDeferred()
+
+ deferred.invokeOnCompletion {
+ if (deferred.isCancelled) {
+ call.cancel()
+ }
+ }
+
+ call.enqueue(object : Callback {
+ override fun onFailure(call: Call, t: Throwable) {
+ deferred.completeExceptionally(t)
+ }
+
+ override fun onResponse(call: Call, response: Response) {
+ if (response.isSuccessful) {
+ deferred.complete(response.body()!!)
+ } else {
+ deferred.completeExceptionally(HttpException(response))
+ }
+ }
+ })
+
+ return deferred
+ }
+ }
+
+ private class ResponseCallAdapter(
+ private val responseType: Type
+ ) : CallAdapter>> {
+
+ override fun responseType() = responseType
+
+ override fun adapt(call: Call): Deferred> {
+ val deferred = CompletableDeferred>()
+
+ deferred.invokeOnCompletion {
+ if (deferred.isCancelled) {
+ call.cancel()
+ }
+ }
+
+ call.enqueue(object : Callback {
+ override fun onFailure(call: Call, t: Throwable) {
+ deferred.completeExceptionally(t)
+ }
+
+ override fun onResponse(call: Call, response: Response) {
+ deferred.complete(response)
+ }
+ })
+
+ return deferred
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt
new file mode 100644
index 000000000..59d92b07d
--- /dev/null
+++ b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt
@@ -0,0 +1,64 @@
+package io.legado.app.help.http
+
+import okhttp3.*
+import retrofit2.Retrofit
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+object HttpHelper {
+
+ val client: OkHttpClient = getOkHttpClient()
+
+
+ fun getApiService(baseUrl: String, clazz: Class): T {
+ return getRetrofit(baseUrl).create(clazz)
+ }
+
+ fun getRetrofit(baseUrl: String): Retrofit {
+ return Retrofit.Builder().baseUrl(baseUrl)
+ //增加返回值为字符串的支持(以实体类返回)
+// .addConverterFactory(EncodeConverter.create())
+ //增加返回值为Observable的支持
+ .addCallAdapterFactory(CoroutinesCallAdapterFactory.invoke())
+ .client(client)
+ .build()
+ }
+
+ private fun getOkHttpClient(): OkHttpClient {
+ val cs = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .tlsVersions(TlsVersion.TLS_1_2)
+ .build()
+
+ val specs = ArrayList()
+ specs.add(cs)
+ specs.add(ConnectionSpec.COMPATIBLE_TLS)
+ specs.add(ConnectionSpec.CLEARTEXT)
+
+ val sslParams = SSLHelper.getSslSocketFactory()
+ return OkHttpClient.Builder()
+ .connectTimeout(15, TimeUnit.SECONDS)
+ .writeTimeout(15, TimeUnit.SECONDS)
+ .readTimeout(15, TimeUnit.SECONDS)
+ .retryOnConnectionFailure(true)
+ .sslSocketFactory(sslParams.sSLSocketFactory, sslParams.trustManager)
+ .hostnameVerifier(SSLHelper.unsafeHostnameVerifier)
+ .connectionSpecs(specs)
+ .followRedirects(true)
+ .followSslRedirects(true)
+ .protocols(listOf(Protocol.HTTP_1_1))
+ .addInterceptor(getHeaderInterceptor())
+ .build()
+ }
+
+ private fun getHeaderInterceptor(): Interceptor {
+ return Interceptor { chain ->
+ val request = chain.request()
+ .newBuilder()
+ .addHeader("Keep-Alive", "300")
+ .addHeader("Connection", "Keep-Alive")
+ .addHeader("Cache-Control", "no-cache")
+ .build()
+ chain.proceed(request)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/help/http/SSLHelper.kt b/app/src/main/java/io/legado/app/help/http/SSLHelper.kt
new file mode 100644
index 000000000..dcf5b7709
--- /dev/null
+++ b/app/src/main/java/io/legado/app/help/http/SSLHelper.kt
@@ -0,0 +1,175 @@
+package io.legado.app.help.http
+
+import javax.net.ssl.*
+import java.io.IOException
+import java.io.InputStream
+import java.security.KeyManagementException
+import java.security.KeyStore
+import java.security.NoSuchAlgorithmException
+import java.security.cert.CertificateException
+import java.security.cert.CertificateFactory
+import java.security.cert.X509Certificate
+
+object SSLHelper {
+
+ val sslSocketFactory: SSLParams
+ get() = getSslSocketFactoryBase(null, null, null)
+
+ /**
+ * 为了解决客户端不信任服务器数字证书的问题,网络上大部分的解决方案都是让客户端不对证书做任何检查,
+ * 这是一种有很大安全漏洞的办法
+ */
+ val unsafeTrustManager: X509TrustManager = object : X509TrustManager {
+ @Throws(CertificateException::class)
+ override fun checkClientTrusted(chain: Array, authType: String) {
+ }
+
+ @Throws(CertificateException::class)
+ override fun checkServerTrusted(chain: Array, authType: String) {
+ }
+
+ override fun getAcceptedIssuers(): Array {
+ return arrayOf()
+ }
+ }
+
+ /**
+ * 此类是用于主机名验证的基接口。 在握手期间,如果 URL 的主机名和服务器的标识主机名不匹配,
+ * 则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。策略可以是基于证书的或依赖于其他验证方案。
+ * 当验证 URL 主机名使用的默认规则失败时使用这些回调。如果主机名是可接受的,则返回 true
+ */
+ val unsafeHostnameVerifier: HostnameVerifier = HostnameVerifier { _, _ -> true }
+
+ class SSLParams {
+ var sSLSocketFactory: SSLSocketFactory? = null
+ var trustManager: X509TrustManager? = null
+ }
+
+ /**
+ * https单向认证
+ * 可以额外配置信任服务端的证书策略,否则默认是按CA证书去验证的,若不是CA可信任的证书,则无法通过验证
+ */
+ fun getSslSocketFactory(trustManager: X509TrustManager): SSLParams {
+ return getSslSocketFactoryBase(trustManager, null, null)
+ }
+
+ /**
+ * https单向认证
+ * 用含有服务端公钥的证书校验服务端证书
+ */
+ fun getSslSocketFactory(vararg certificates: InputStream): SSLParams {
+ return getSslSocketFactoryBase(null, null, null, *certificates)
+ }
+
+ /**
+ * https双向认证
+ * bksFile 和 password -> 客户端使用bks证书校验服务端证书
+ * certificates -> 用含有服务端公钥的证书校验服务端证书
+ */
+ fun getSslSocketFactory(bksFile: InputStream, password: String, vararg certificates: InputStream): SSLParams {
+ return getSslSocketFactoryBase(null, bksFile, password, *certificates)
+ }
+
+ /**
+ * https双向认证
+ * bksFile 和 password -> 客户端使用bks证书校验服务端证书
+ * X509TrustManager -> 如果需要自己校验,那么可以自己实现相关校验,如果不需要自己校验,那么传null即可
+ */
+ fun getSslSocketFactory(bksFile: InputStream, password: String, trustManager: X509TrustManager): SSLParams {
+ return getSslSocketFactoryBase(trustManager, bksFile, password)
+ }
+
+ private fun getSslSocketFactoryBase(
+ trustManager: X509TrustManager?,
+ bksFile: InputStream?,
+ password: String?,
+ vararg certificates: InputStream
+ ): SSLParams {
+ val sslParams = SSLParams()
+ try {
+ val keyManagers = prepareKeyManager(bksFile, password)
+ val trustManagers = prepareTrustManager(*certificates)
+ val manager: X509TrustManager?
+ manager = //优先使用用户自定义的TrustManager
+ trustManager ?: if (trustManagers != null) {
+ //然后使用默认的TrustManager
+ chooseTrustManager(trustManagers)
+ } else {
+ //否则使用不安全的TrustManager
+ unsafeTrustManager
+ }
+ // 创建TLS类型的SSLContext对象, that uses our TrustManager
+ val sslContext = SSLContext.getInstance("TLS")
+ // 用上面得到的trustManagers初始化SSLContext,这样sslContext就会信任keyStore中的证书
+ // 第一个参数是授权的密钥管理器,用来授权验证,比如授权自签名的证书验证。第二个是被授权的证书管理器,用来验证服务器端的证书
+ sslContext.init(keyManagers, manager?.let { arrayOf(it) }, null)
+ // 通过sslContext获取SSLSocketFactory对象
+ sslParams.sSLSocketFactory = sslContext.socketFactory
+ sslParams.trustManager = manager
+ return sslParams
+ } catch (e: NoSuchAlgorithmException) {
+ throw AssertionError(e)
+ } catch (e: KeyManagementException) {
+ throw AssertionError(e)
+ }
+
+ }
+
+ private fun prepareKeyManager(bksFile: InputStream?, password: String?): Array? {
+ try {
+ if (bksFile == null || password == null) return null
+ val clientKeyStore = KeyStore.getInstance("BKS")
+ clientKeyStore.load(bksFile, password.toCharArray())
+ val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
+ kmf.init(clientKeyStore, password.toCharArray())
+ return kmf.keyManagers
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ return null
+ }
+
+ private fun prepareTrustManager(vararg certificates: InputStream): Array? {
+ if (certificates.isEmpty()) return null
+ try {
+ val certificateFactory = CertificateFactory.getInstance("X.509")
+ // 创建一个默认类型的KeyStore,存储我们信任的证书
+ val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
+ keyStore.load(null)
+ var index = 0
+ for (certStream in certificates) {
+ val certificateAlias = Integer.toString(index++)
+ // 证书工厂根据证书文件的流生成证书 cert
+ val cert = certificateFactory.generateCertificate(certStream)
+ // 将 cert 作为可信证书放入到keyStore中
+ keyStore.setCertificateEntry(certificateAlias, cert)
+ try {
+ certStream?.close()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+
+ }
+ //我们创建一个默认类型的TrustManagerFactory
+ val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
+ //用我们之前的keyStore实例初始化TrustManagerFactory,这样tmf就会信任keyStore中的证书
+ tmf.init(keyStore)
+ //通过tmf获取TrustManager数组,TrustManager也会信任keyStore中的证书
+ return tmf.trustManagers
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ return null
+ }
+
+ private fun chooseTrustManager(trustManagers: Array): X509TrustManager? {
+ for (trustManager in trustManagers) {
+ if (trustManager is X509TrustManager) {
+ return trustManager
+ }
+ }
+ return null
+ }
+}
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 d04315dd6..ce40a7e33 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
@@ -7,26 +7,26 @@ import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
-import androidx.lifecycle.ViewModelProvider
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.getViewModel
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar_main.*
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
override val viewModel: MainViewModel
- get() = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(MainViewModel::class.java)
+ get() = getViewModel(MainViewModel::class.java)
+
override val layoutID: Int
get() = R.layout.activity_main
override fun onViewModelCreated(viewModel: MainViewModel, savedInstanceState: Bundle?) {
- setSupportActionBar(toolbar)
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
)
@@ -58,21 +58,6 @@ class MainActivity : BaseActivity(), NavigationV
override fun onNavigationItemSelected(item: MenuItem): Boolean {
// Handle navigation view item clicks here.
when (item.itemId) {
- R.id.nav_home -> {
- // Handle the camera action
- }
- R.id.nav_gallery -> {
-
- }
- R.id.nav_slideshow -> {
-
- }
- R.id.nav_tools -> {
-
- }
- R.id.nav_share -> {
-
- }
R.id.nav_send -> {
}
diff --git a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt
new file mode 100644
index 000000000..304000c53
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt
@@ -0,0 +1,53 @@
+package io.legado.app.ui.replacerule
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import androidx.paging.LivePagedListBuilder
+import androidx.paging.PagedList
+import androidx.recyclerview.widget.LinearLayoutManager
+import io.legado.app.App
+import io.legado.app.R
+import io.legado.app.data.entities.ReplaceRule
+import kotlinx.android.synthetic.main.activity_replace_rule.*
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.toast
+
+
+class ReplaceRuleActivity : AppCompatActivity() {
+ private lateinit var adapter: ReplaceRuleAdapter
+ private var rulesLiveData: LiveData>? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_replace_rule)
+ rv_replace_rule.layoutManager = LinearLayoutManager(this)
+ initRecyclerView()
+ initDataObservers()
+ }
+
+ private fun initRecyclerView() {
+ rv_replace_rule.layoutManager = LinearLayoutManager(this)
+ adapter = ReplaceRuleAdapter(this)
+ adapter.onClickListener = object: ReplaceRuleAdapter.OnClickListener {
+ override fun update(rule: ReplaceRule) {
+ doAsync { App.db.replaceRuleDao().update(rule) }
+ }
+ override fun delete(rule: ReplaceRule) {
+ doAsync { App.db.replaceRuleDao().delete(rule) }
+ }
+ override fun edit(rule: ReplaceRule) {
+ toast("Edit function not implemented!")
+ }
+ }
+ rv_replace_rule.adapter = adapter
+ }
+
+ private fun initDataObservers() {
+ rulesLiveData?.removeObservers(this)
+ rulesLiveData = LivePagedListBuilder(App.db.replaceRuleDao().observeAll(), 30).build()
+ rulesLiveData?.observe(this, Observer> { adapter.submitList(it) })
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt
new file mode 100644
index 000000000..717e2bfff
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt
@@ -0,0 +1,70 @@
+package io.legado.app.ui.replacerule
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isGone
+import androidx.paging.PagedListAdapter
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import io.legado.app.R
+import io.legado.app.data.entities.ReplaceRule
+import kotlinx.android.synthetic.main.item_relace_rule.view.*
+import org.jetbrains.anko.sdk27.listeners.onClick
+
+
+class ReplaceRuleAdapter(context: Context) :
+ PagedListAdapter(DIFF_CALLBACK) {
+
+ var onClickListener: OnClickListener? = null
+
+ companion object {
+
+ @JvmField
+ val DIFF_CALLBACK = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean =
+ oldItem.id == newItem.id
+
+ override fun areContentsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean =
+ oldItem.id == newItem.id
+ && oldItem.pattern == newItem.pattern
+ && oldItem.replacement == newItem.replacement
+ && oldItem.isRegex == newItem.isRegex
+ && oldItem.isEnabled == newItem.isEnabled
+ && oldItem.scope == newItem.scope
+ }
+ }
+
+ init {
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
+ return MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_relace_rule, parent, false))
+ }
+
+ override fun onBindViewHolder(holder: MyViewHolder, pos: Int) {
+ getItem(pos)?.let { holder.bind(it, onClickListener, pos == itemCount - 1) }
+ }
+
+ class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ fun bind(rule: ReplaceRule, listener: OnClickListener?, hideDivider: Boolean) = with(itemView) {
+ cb_enable.text = rule.name
+ cb_enable.isChecked = rule.isEnabled
+ divider.isGone = hideDivider
+ iv_delete.onClick { listener?.delete(rule) }
+ iv_edit.onClick { listener?.edit(rule) }
+ cb_enable.onClick {
+ rule.isEnabled = cb_enable.isChecked
+ listener?.update(rule)
+ }
+ }
+ }
+
+ interface OnClickListener {
+ fun update(rule: ReplaceRule)
+ fun delete(rule: ReplaceRule)
+ fun edit(rule: ReplaceRule)
+ }
+}
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 2d4f64ba2..226c9207d 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
@@ -1,21 +1,19 @@
package io.legado.app.ui.search
import android.os.Bundle
-import androidx.lifecycle.ViewModelProvider
import io.legado.app.R
import io.legado.app.base.BaseActivity
+import io.legado.app.utils.getViewModel
class SearchActivity : BaseActivity() {
override val viewModel: SearchViewModel
- get() = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(SearchViewModel::class.java)
+ get() = getViewModel(SearchViewModel::class.java)
override val layoutID: Int
get() = R.layout.activity_search
override fun onViewModelCreated(viewModel: SearchViewModel, savedInstanceState: Bundle?) {
-
-
}
}
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/search/SearchViewModel.kt b/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt
index a9a847a5b..0f9f2072b 100644
--- a/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt
+++ b/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt
@@ -1,6 +1,39 @@
package io.legado.app.ui.search
import android.app.Application
-import androidx.lifecycle.AndroidViewModel
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import io.legado.app.base.BaseViewModel
+import io.legado.app.data.api.CommonHttpApi
+import io.legado.app.data.entities.SearchBook
+import io.legado.app.help.http.HttpHelper
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.withContext
-class SearchViewModel(application: Application) : AndroidViewModel(application)
+class SearchViewModel(application: Application) : BaseViewModel(application) {
+
+ val searchBooks: LiveData> = MutableLiveData()
+
+ public fun search(start: () -> Unit, finally: () -> Unit) {
+ launchOnUI(
+ {
+ start()
+ val searchResponse = withContext(IO) {
+ HttpHelper.getApiService(
+ "http:www.baidu.com",
+ CommonHttpApi::class.java
+ ).get("", mutableMapOf())
+ }
+
+ val result = searchResponse.await()
+ },
+ { Log.i("TAG", "${it.message}") },
+ { finally() })
+
+// GlobalScope.launch {
+//
+// }
+ }
+
+}
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/FileUtils.kt b/app/src/main/java/io/legado/app/utils/FileUtils.kt
new file mode 100644
index 000000000..493347429
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/FileUtils.kt
@@ -0,0 +1,5 @@
+package io.legado.app.utils
+
+import android.os.Environment
+
+fun getSdPath() = Environment.getExternalStorageDirectory().absolutePath
diff --git a/app/src/main/java/io/legado/app/utils/MiscExtensions.kt b/app/src/main/java/io/legado/app/utils/MiscExtensions.kt
new file mode 100644
index 000000000..24bf95c9a
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/MiscExtensions.kt
@@ -0,0 +1,9 @@
+package io.legado.app.utils
+
+import com.jayway.jsonpath.ReadContext
+
+fun ReadContext.readString(path: String) = this.read(path, String::class.java)
+
+fun ReadContext.readBool(path: String) = this.read(path, Boolean::class.java)
+
+fun ReadContext.readInt(path: String) = this.read(path, Int::class.java)
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/utils/ViewModelExtensions.kt b/app/src/main/java/io/legado/app/utils/ViewModelExtensions.kt
new file mode 100644
index 000000000..ed963ec8e
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/ViewModelExtensions.kt
@@ -0,0 +1,12 @@
+package io.legado.app.utils
+
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProviders
+
+fun AppCompatActivity.getViewModel(clazz: Class) = ViewModelProviders.of(this).get(clazz)
+
+fun Fragment.getViewModel(clazz: Class) = ViewModelProviders.of(this).get(clazz)
+
+fun Fragment.getViewModelOfActivity(clazz: Class) = ViewModelProviders.of(requireActivity()).get(clazz)
diff --git a/app/src/main/res/drawable/bg_ib_pre_round.xml b/app/src/main/res/drawable/bg_ib_pre_round.xml
new file mode 100644
index 000000000..a370bb64b
--- /dev/null
+++ b/app/src/main/res/drawable/bg_ib_pre_round.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_clear_all.xml b/app/src/main/res/drawable/ic_clear_all.xml
new file mode 100644
index 000000000..dfa860c00
--- /dev/null
+++ b/app/src/main/res/drawable/ic_clear_all.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml
new file mode 100644
index 000000000..2d249dde4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_edit.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_replace_rule.xml b/app/src/main/res/layout/activity_replace_rule.xml
new file mode 100644
index 000000000..773e22242
--- /dev/null
+++ b/app/src/main/res/layout/activity_replace_rule.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml
index a7dfaad9b..9e2a9e07c 100644
--- a/app/src/main/res/layout/activity_search.xml
+++ b/app/src/main/res/layout/activity_search.xml
@@ -2,7 +2,6 @@
-
+ app:title="搜索"/>
-
-
-
-
-
+ android:layout_height="wrap_content"/>
diff --git a/app/src/main/res/layout/item_relace_rule.xml b/app/src/main/res/layout/item_relace_rule.xml
new file mode 100644
index 000000000..80295a529
--- /dev/null
+++ b/app/src/main/res/layout/item_relace_rule.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
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/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml
index 9df2b3d76..3a491985e 100644
--- a/app/src/main/res/menu/activity_main_drawer.xml
+++ b/app/src/main/res/menu/activity_main_drawer.xml
@@ -5,29 +5,29 @@
+ android:title="@string/menu_backup"/>
+ android:title="@string/menu_import"/>
+ android:title="@string/menu_import_old"/>
+ android:title="@string/menu_import_github"/>
-