Merge pull request #31 from gedoor/master

update
pull/481/head
Antecer 6 years ago committed by GitHub
commit a65ad9a2f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      app/build.gradle
  2. 1
      app/src/main/AndroidManifest.xml
  3. 6
      app/src/main/java/io/legado/app/App.kt
  4. 52
      app/src/main/java/io/legado/app/base/BaseViewModel.kt
  5. 405
      app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt
  6. 32
      app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt
  7. 91
      app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt
  8. 16
      app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt
  9. 9
      app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt
  10. 26
      app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt
  11. 17
      app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt
  12. 13
      app/src/main/java/io/legado/app/base/adapter/animations/BaseAnimation.kt
  13. 20
      app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt
  14. 13
      app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt
  15. 14
      app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt
  16. 14
      app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt
  17. 4
      app/src/main/java/io/legado/app/constant/AppConst.kt
  18. 2
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  19. 14
      app/src/main/java/io/legado/app/data/api/CommonHttpApi.kt
  20. 49
      app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt
  21. 18
      app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt
  22. 1
      app/src/main/java/io/legado/app/data/entities/SearchBook.kt
  23. 54
      app/src/main/java/io/legado/app/help/LayoutManager.kt
  24. 107
      app/src/main/java/io/legado/app/help/http/CoroutinesCallAdapterFactory.kt
  25. 64
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  26. 175
      app/src/main/java/io/legado/app/help/http/SSLHelper.kt
  27. 23
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  28. 53
      app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt
  29. 70
      app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt
  30. 6
      app/src/main/java/io/legado/app/ui/search/SearchActivity.kt
  31. 34
      app/src/main/java/io/legado/app/ui/search/SearchAdapter.kt
  32. 37
      app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt
  33. 83
      app/src/main/java/io/legado/app/ui/widget/TitleBar.kt
  34. 5
      app/src/main/java/io/legado/app/utils/FileUtils.kt
  35. 9
      app/src/main/java/io/legado/app/utils/MiscExtensions.kt
  36. 12
      app/src/main/java/io/legado/app/utils/ViewModelExtensions.kt
  37. 4
      app/src/main/res/drawable/bg_ib_pre_round.xml
  38. 17
      app/src/main/res/drawable/ic_clear_all.xml
  39. 14
      app/src/main/res/drawable/ic_edit.xml
  40. 30
      app/src/main/res/layout/activity_replace_rule.xml
  41. 3
      app/src/main/res/layout/activity_search.xml
  42. 15
      app/src/main/res/layout/app_bar_main.xml
  43. 57
      app/src/main/res/layout/item_relace_rule.xml
  44. 19
      app/src/main/res/layout/item_search.xml
  45. 1
      app/src/main/res/layout/view_titlebar.xml
  46. 20
      app/src/main/res/menu/activity_main_drawer.xml
  47. 8
      app/src/main/res/values/attrs.xml
  48. 2
      app/src/main/res/values/colors.xml
  49. 13
      app/src/main/res/values/strings.xml

@ -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'

@ -39,6 +39,7 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".ui.replacerule.ReplaceRuleActivity"/>
</application>
</manifest>

@ -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)
}
}

@ -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<Job> = 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()
}
}

@ -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 可添加headerfooter以及不同类型item
*/
abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : RecyclerView.Adapter<ItemViewHolder>() {
constructor(context: Context, vararg delegates: Pair<Int, ItemViewDelegate<ITEM>>) : this(context) {
addItemViewDelegates(*delegates)
}
constructor(context: Context, vararg delegates: ItemViewDelegate<ITEM>) : this(context) {
addItemViewDelegates(*delegates)
}
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var headerItems: SparseArray<View>? = null
private var footerItems: SparseArray<View>? = null
private val itemDelegates: HashMap<Int, ItemViewDelegate<ITEM>> = hashMapOf()
private val items: MutableList<ITEM> = mutableListOf()
private val lock = Object()
private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null
private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null
private var itemAnimation: ItemAnimation? = null
fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) {
itemClickListener = listener
}
fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) {
itemLongClickListener = listener
}
fun bindToRecyclerView(recyclerView: RecyclerView) {
recyclerView.adapter = this
}
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegate(viewType: Int, delegate: DELEGATE) {
itemDelegates.put(viewType, delegate)
}
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegate(delegate: DELEGATE) {
itemDelegates.put(itemDelegates.size, delegate)
}
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegates(vararg delegates: DELEGATE) {
delegates.forEach {
addItemViewDelegate(it)
}
}
fun addItemViewDelegates(vararg delegates: Pair<Int, ItemViewDelegate<ITEM>>) {
delegates.forEach {
addItemViewDelegate(it.first, it.second)
}
}
fun addHeaderView(header: View) {
synchronized(lock) {
if (headerItems == null) {
headerItems = SparseArray()
}
headerItems?.let {
val index = it.size()
it.put(TYPE_HEADER_VIEW + it.size(), header)
notifyItemInserted(index)
}
}
}
fun addFooterView(footer: View) {
synchronized(lock) {
if (footerItems == null) {
footerItems = SparseArray()
}
footerItems?.let {
val index = getActualItemCount() + it.size()
it.put(TYPE_FOOTER_VIEW + it.size(), footer)
notifyItemInserted(index)
}
}
}
fun removeHeaderView(header: View) {
synchronized(lock) {
headerItems?.let {
val index = it.indexOfValue(header)
if (index >= 0) {
it.remove(index)
notifyItemRemoved(index)
}
}
}
}
fun removeFooterView(footer: View) {
synchronized(lock) {
footerItems?.let {
val index = it.indexOfValue(footer)
if (index >= 0) {
it.remove(index)
notifyItemRemoved(getActualItemCount() + index - 2)
}
}
}
}
fun setItems(items: List<ITEM>?) {
synchronized(lock) {
if (this.items.isNotEmpty()) {
this.items.clear()
}
if (items != null) {
this.items.addAll(items)
}
notifyDataSetChanged()
}
}
fun setItem(position: Int, item: ITEM) {
synchronized(lock) {
val oldSize = getActualItemCount()
if (position in 0 until oldSize) {
this.items[position] = item
notifyItemChanged(position + getHeaderCount())
}
}
}
fun addItem(item: ITEM) {
synchronized(lock) {
val oldSize = getActualItemCount()
if (this.items.add(item)) {
if (oldSize == 0) {
notifyDataSetChanged()
} else {
notifyItemInserted(oldSize + getHeaderCount())
}
}
}
}
fun addItems(position: Int, newItems: List<ITEM>) {
synchronized(lock) {
val oldSize = getActualItemCount()
if (position in 0 until oldSize) {
if (if (oldSize == 0) this.items.addAll(newItems) else this.items.addAll(position, newItems)) {
if (oldSize == 0) {
notifyDataSetChanged()
} else {
notifyItemRangeChanged(position + getHeaderCount(), newItems.size)
}
}
}
}
}
fun addItems(newItems: List<ITEM>) {
synchronized(lock) {
val oldSize = getActualItemCount()
if (this.items.addAll(newItems)) {
if (oldSize == 0) {
notifyDataSetChanged()
} else {
notifyItemRangeChanged(oldSize + getHeaderCount(), newItems.size)
}
}
}
}
fun removeItem(position: Int) {
synchronized(lock) {
if (this.items.removeAt(position) != null)
notifyItemRemoved(position + getHeaderCount())
}
}
fun removeItem(item: ITEM) {
synchronized(lock) {
if (this.items.remove(item))
notifyItemRemoved(this.items.indexOf(item) + getHeaderCount())
}
}
fun removeItems(items: List<ITEM>) {
synchronized(lock) {
if (this.items.removeAll(items))
notifyDataSetChanged()
}
}
fun swapItem(oldPosition: Int, newPosition: Int) {
synchronized(lock) {
val size = getActualItemCount()
if (oldPosition in 0 until size && newPosition in 0 until size) {
Collections.swap(this.items, oldPosition + getHeaderCount(), newPosition + getHeaderCount())
notifyDataSetChanged()
}
}
}
fun updateItem(position: Int, payload: Any) {
synchronized(lock) {
val size = getActualItemCount()
if (position in 0 until size) {
notifyItemChanged(position + getHeaderCount(), payload)
}
}
}
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) {
synchronized(lock) {
val size = getActualItemCount()
if (fromPosition in 0 until size && toPosition in 0 until size) {
notifyItemRangeChanged(fromPosition + getHeaderCount(), toPosition - fromPosition + 1, payloads)
}
}
}
fun isEmpty(): Boolean {
return items.isEmpty()
}
fun isNotEmpty(): Boolean {
return items.isNotEmpty()
}
/**
* 除去header和footer
*/
fun getActualItemCount(): Int {
return items.size
}
fun getHeaderCount(): Int {
return headerItems?.size() ?: 0
}
fun getFooterCount(): Int {
return footerItems?.size() ?: 0
}
fun getItem(position: Int): ITEM? = if (position in 0 until items.size) items[position] else null
fun getItems(): List<ITEM> = items
protected open fun getItemViewType(item: ITEM, position: Int): Int {
return 0
}
/**
* grid 模式下使用
*/
protected open fun getSpanSize(item: ITEM, viewType: Int, position: Int): Int {
return 1
}
final override fun getItemCount(): Int {
return getActualItemCount() + getHeaderCount() + getFooterCount()
}
final override fun getItemViewType(position: Int): Int {
return when {
isHeader(position) -> TYPE_HEADER_VIEW + position
isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount()
else -> 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<Any>) {
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
}
}

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

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

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

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

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

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

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

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

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

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

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

@ -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
}

@ -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
}

@ -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<String, String>): Deferred<String>
@FormUrlEncoded
@POST
fun post(@Url url: String, @FieldMap map: Map<String, String>): Deferred<String>
}

@ -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<Int, ReplaceRule>
@Query("SELECT id FROM replace_rules ORDER BY sortOrder ASC")
fun observeAllIds(): LiveData<List<Int>>
@get:Query("SELECT MAX(sortOrder) FROM replace_rules")
val maxOrder: Int
@get:Query("SELECT * FROM replace_rules ORDER BY sortOrder ASC")
val all: List<ReplaceRule>
@get:Query("SELECT * FROM replace_rules WHERE isEnabled = 1 ORDER BY sortOrder ASC")
val allEnabled: List<ReplaceRule>
@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<ReplaceRule>
@Query("SELECT * FROM replace_rules WHERE isEnabled = 1 AND scope LIKE '%' || :scope || '%'")
fun findEnabledByScope(scope: String): List<ReplaceRule>
@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)
}

@ -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

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

@ -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)
}
}
}

@ -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<out Annotation>,
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<Foo> or Deferred<out Foo>")
}
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<Foo> or Response<out Foo>")
}
ResponseCallAdapter<Any>(
getParameterUpperBound(
0,
responseType
)
)
} else {
BodyCallAdapter<Any>(responseType)
}
}
private class BodyCallAdapter<T>(
private val responseType: Type
) : CallAdapter<T, Deferred<T>> {
override fun responseType() = responseType
override fun adapt(call: Call<T>): Deferred<T> {
val deferred = CompletableDeferred<T>()
deferred.invokeOnCompletion {
if (deferred.isCancelled) {
call.cancel()
}
}
call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
deferred.completeExceptionally(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
deferred.complete(response.body()!!)
} else {
deferred.completeExceptionally(HttpException(response))
}
}
})
return deferred
}
}
private class ResponseCallAdapter<T>(
private val responseType: Type
) : CallAdapter<T, Deferred<Response<T>>> {
override fun responseType() = responseType
override fun adapt(call: Call<T>): Deferred<Response<T>> {
val deferred = CompletableDeferred<Response<T>>()
deferred.invokeOnCompletion {
if (deferred.isCancelled) {
call.cancel()
}
}
call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
deferred.completeExceptionally(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
deferred.complete(response)
}
})
return deferred
}
}
}

@ -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 <T> getApiService(baseUrl: String, clazz: Class<T>): T {
return getRetrofit(baseUrl).create(clazz)
}
fun getRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder().baseUrl(baseUrl)
//增加返回值为字符串的支持(以实体类返回)
// .addConverterFactory(EncodeConverter.create())
//增加返回值为Observable<T>的支持
.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<ConnectionSpec>()
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)
}
}
}

@ -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<X509Certificate>, authType: String) {
}
@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
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<TrustManager>(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<KeyManager>? {
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<TrustManager>? {
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<TrustManager>): X509TrustManager? {
for (trustManager in trustManagers) {
if (trustManager is X509TrustManager) {
return trustManager
}
}
return null
}
}

@ -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<MainDataBinding, MainViewModel>(), 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<MainDataBinding, MainViewModel>(), 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 -> {
}

@ -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<PagedList<ReplaceRule>>? = 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<PagedList<ReplaceRule>> { adapter.submitList(it) })
}
}

@ -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<ReplaceRule, ReplaceRuleAdapter.MyViewHolder>(DIFF_CALLBACK) {
var onClickListener: OnClickListener? = null
companion object {
@JvmField
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ReplaceRule>() {
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)
}
}

@ -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<SearchDataBinding, SearchViewModel>() {
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?) {
}
}

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

@ -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<List<SearchBook>> = 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 {
//
// }
}
}

@ -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)
}
}

@ -0,0 +1,5 @@
package io.legado.app.utils
import android.os.Environment
fun getSdPath() = Environment.getExternalStorageDirectory().absolutePath

@ -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)

@ -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 <T : ViewModel> AppCompatActivity.getViewModel(clazz: Class<T>) = ViewModelProviders.of(this).get(clazz)
fun <T : ViewModel> Fragment.getViewModel(clazz: Class<T>) = ViewModelProviders.of(this).get(clazz)
fun <T : ViewModel> Fragment.getViewModelOfActivity(clazz: Class<T>) = ViewModelProviders.of(requireActivity()).get(clazz)

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/transparent"/>
</selector>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M14.434,20 L9.566,20 C7.535,20,5.863,18.665,5.761,16.961 L5.18,7.199 L4,7.199 L4,5.919 L8.402,5.919 L8.644,5.315 C8.957,4.526,9.827,4,10.813,4 L13.188,4 C14.172,4,15.043,4.526,15.355,5.313 L15.598,5.918 L20,5.918 L20,7.198 L18.82,7.198 L18.238,16.96 C18.137,18.665,16.466,20,14.434,20 Z M6.706,7.199 L7.283,16.898 C7.344,17.917,8.347,18.719,9.566,18.719 L14.433,18.719 C15.653,18.719,16.656,17.916,16.716,16.898 L17.293,7.199 L6.706,7.199 Z M10.01,5.919 L13.99,5.919 L13.91,5.718 C13.806,5.457,13.515,5.28,13.187,5.28 L10.812,5.28 C10.484,5.28,10.193,5.457,10.089,5.718 L10.01,5.919 Z" />
<path
android:fillColor="#595757"
android:pathData="M9.542,16.509 L9.127,8.83 L10.648,8.773 L11.064,16.449 L9.542,16.509 Z" />
<path
android:fillColor="#595757"
android:pathData="M14.457,16.509 L12.936,16.449 L13.352,8.773 L14.873,8.83 L14.457,16.509 Z" />
</vector>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#595757"
android:pathData="M5.6,20.009c-0.881,0-1.6-0.789-1.6-1.758V5.749C4,4.78,4.719,3.991,5.6,3.991h10.62 c0.882,0,1.599,0.789,1.599,1.758v1.258h-1.483l-0.076-1.258c0-0.122-0.062-0.172-0.062-0.172L5.6,5.582 c0.002,0.015-0.039,0.069-0.039,0.167v12.502c0,0.107,0.051,0.164,0.063,0.172l10.596-0.005c-0.002-0.014,0.039-0.067,0.039-0.167 l0.016-4.739h1.469l0.075,4.739c0,0.969-0.717,1.758-1.599,1.758H5.6z" />
<path
android:fillColor="#595757"
android:pathData="M 12.549 13.354 L 13.738 12.323 L 13.738 12.323 L 18.967 7.553 L 20 8.646 L 14.658 13.514 L 13.54 14.515 Z" />
</vector>

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="io.legado.app.ui.search.SearchActivity">
<io.legado.app.ui.widget.TitleBar
android:id="@+id/titleBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:title="净化替换"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_replace_rule"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleBar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.0"/>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -2,7 +2,6 @@
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data class=".ui.search.SearchDataBinding">
<variable name="SearchViewModel" type="io.legado.app.ui.search.SearchViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@ -17,7 +16,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
/>
app:title="搜索"/>
<io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout
android:layout_width="match_parent"

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

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/cb_enable"
android:text="Rule Name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:textSize="16sp"
android:textColor="@color/text_default"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/iv_delete"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:background="@drawable/bg_ib_pre_round"
android:contentDescription="@string/edit"
android:src="@drawable/ic_edit"
app:tint="@color/text_default"
app:layout_constraintEnd_toStartOf="@id/iv_delete"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:background="@drawable/bg_ib_pre_round"
android:contentDescription="@string/delete"
android:src="@drawable/ic_clear_all"
app:tint="@color/text_default"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@color/divider"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

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

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

@ -5,29 +5,29 @@
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_home"
android:id="@+id/nav_backup"
android:icon="@drawable/ic_menu_camera"
android:title="@string/menu_home"/>
android:title="@string/menu_backup"/>
<item
android:id="@+id/nav_gallery"
android:id="@+id/nav_import"
android:icon="@drawable/ic_menu_gallery"
android:title="@string/menu_gallery"/>
android:title="@string/menu_import"/>
<item
android:id="@+id/nav_slideshow"
android:id="@+id/nav_import_old"
android:icon="@drawable/ic_menu_slideshow"
android:title="@string/menu_slideshow"/>
android:title="@string/menu_import_old"/>
<item
android:id="@+id/nav_tools"
android:id="@+id/nav_import_github"
android:icon="@drawable/ic_menu_manage"
android:title="@string/menu_tools"/>
android:title="@string/menu_import_github"/>
</group>
<item android:title="Communicate">
<menu>
<item
android:id="@+id/nav_share"
android:id="@+id/nav_replace_rule"
android:icon="@drawable/ic_menu_share"
android:title="@string/menu_share"/>
android:title="@string/menu_replace_rule"/>
<item
android:id="@+id/nav_send"
android:icon="@drawable/ic_menu_send"

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

@ -3,6 +3,8 @@
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
<color name="text_default">#222222</color>
<color name="divider">#66666666</color>
</resources>

@ -7,14 +7,17 @@
<string name="nav_header_desc">Navigation header</string>
<string name="action_settings">Settings</string>
<string name="menu_home">Home</string>
<string name="menu_gallery">Gallery</string>
<string name="menu_slideshow">Slideshow</string>
<string name="menu_tools">Tools</string>
<string name="menu_share">Share</string>
<string name="menu_backup">Home</string>
<string name="menu_import">导入</string>
<string name="menu_import_old">导入阅读数据</string>
<string name="menu_import_github">导入Github数据</string>
<string name="menu_replace_rule">净化替换</string>
<string name="menu_send">Send</string>
<string name="dynamic_click_retry">点击重试</string>
<string name="dynamic_loading">正在加载</string>
<string name="perm_request_storage">阅读需要访问存储卡权限:</string>
<string name="edit">编辑</string>
<string name="delete">删除</string>
</resources>

Loading…
Cancel
Save