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"/> + android:title="@string/menu_replace_rule"/> + + + + + + @@ -29,6 +35,8 @@ + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7e091a230..8194c2ece 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,6 +3,8 @@ #008577 #00574B #D81B60 + #222222 + #66666666 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7def64d4..05315f8a9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,14 +7,17 @@ Navigation header Settings - Home - Gallery - Slideshow - Tools - Share + Home + 导入 + 导入阅读数据 + 导入Github数据 + 净化替换 Send 点击重试 正在加载 + 阅读需要访问存储卡权限: + 编辑 + 删除