Merge branch 'master' into urischeme

pull/35/head
kunfei 5 years ago committed by GitHub
commit 8be436f6c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      app/src/main/AndroidManifest.xml
  2. 22
      app/src/main/java/io/legado/app/base/BaseViewModel.kt
  3. 10
      app/src/main/java/io/legado/app/constant/AppConst.kt
  4. 2
      app/src/main/java/io/legado/app/constant/RSSKeywords.kt
  5. 3
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  6. 3
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  7. 13
      app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
  8. 10
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  9. 18
      app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt
  10. 31
      app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt
  11. 15
      app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt
  12. 3
      app/src/main/java/io/legado/app/data/entities/EditEntity.kt
  13. 15
      app/src/main/java/io/legado/app/data/entities/RssArticle.kt
  14. 20
      app/src/main/java/io/legado/app/data/entities/RssSource.kt
  15. 2
      app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt
  16. 37
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  17. 32
      app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt
  18. 26
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  19. 4
      app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt
  20. 24
      app/src/main/java/io/legado/app/model/Rss.kt
  21. 40
      app/src/main/java/io/legado/app/model/WebBook.kt
  22. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  23. 6
      app/src/main/java/io/legado/app/model/rss/RssParser.kt
  24. 94
      app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt
  25. 3
      app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt
  26. 2
      app/src/main/java/io/legado/app/ui/about/AboutFragment.kt
  27. 5
      app/src/main/java/io/legado/app/ui/about/DonateActivity.kt
  28. 30
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  29. 3
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  30. 24
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  31. 3
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  32. 5
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  33. 4
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  34. 24
      app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt
  35. 42
      app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt
  36. 80
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  37. 8
      app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt
  38. 35
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  39. 13
      app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt
  40. 2
      app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt
  41. 3
      app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt
  42. 369
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  43. 7
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt
  44. 27
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt
  45. 622
      app/src/main/java/io/legado/app/ui/book/source/edit/SourceEditActivity.kt
  46. 14
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  47. 3
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  48. 1
      app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt
  49. 19
      app/src/main/java/io/legado/app/ui/changesource/ChangeSourceDialog.kt
  50. 12
      app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt
  51. 2
      app/src/main/java/io/legado/app/ui/config/ConfigFragment.kt
  52. 4
      app/src/main/java/io/legado/app/ui/explore/ExploreShowAdapter.kt
  53. 2
      app/src/main/java/io/legado/app/ui/explore/ExploreShowViewModel.kt
  54. 3
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  55. 6
      app/src/main/java/io/legado/app/ui/main/bookshelf/BooksAdapter.kt
  56. 6
      app/src/main/java/io/legado/app/ui/main/bookshelf/BooksFragment.kt
  57. 54
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  58. 25
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  59. 176
      app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageDialog.kt
  60. 4
      app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt
  61. 8
      app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt
  62. 15
      app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt
  63. 32
      app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt
  64. 1
      app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt
  65. 2
      app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt
  66. 2
      app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditViewModel.kt
  67. 69
      app/src/main/java/io/legado/app/ui/rss/article/RssArticlesActivity.kt
  68. 44
      app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt
  69. 45
      app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt
  70. 13
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  71. 2
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt
  72. 14
      app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugActivity.kt
  73. 195
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt
  74. 85
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt
  75. 57
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt
  76. 41
      app/src/main/java/io/legado/app/ui/rss/source/manage/DiffCallBack.kt
  77. 67
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  78. 79
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt
  79. 44
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt
  80. 8
      app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt
  81. 8
      app/src/main/java/io/legado/app/ui/widget/anima/RefreshProgressBar.kt
  82. 5
      app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/Utils.kt
  83. 12
      app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt
  84. 6
      app/src/main/java/io/legado/app/ui/widget/page/ContentView.kt
  85. 110
      app/src/main/java/io/legado/app/utils/ACache.kt
  86. 255
      app/src/main/java/io/legado/app/utils/BitmapUtil.kt
  87. 80
      app/src/main/java/io/legado/app/utils/LogUtils.kt
  88. 0
      app/src/main/res/drawable/image_cover_default.jpg
  89. 0
      app/src/main/res/drawable/image_cover_gs.jpg
  90. BIN
      app/src/main/res/drawable/image_rss.jpg
  91. 31
      app/src/main/res/layout-land/activity_book_info.xml
  92. 2
      app/src/main/res/layout/activity_audio_play.xml
  93. 33
      app/src/main/res/layout/activity_book_info.xml
  94. 2
      app/src/main/res/layout/activity_book_info_edit.xml
  95. 0
      app/src/main/res/layout/activity_book_read.xml
  96. 55
      app/src/main/res/layout/activity_book_search.xml
  97. 7
      app/src/main/res/layout/activity_read_rss.xml
  98. 34
      app/src/main/res/layout/activity_rss_artivles.xml
  99. 16
      app/src/main/res/layout/activity_rss_read.xml
  100. 6
      app/src/main/res/layout/activity_rss_source_edit.xml
  101. Some files were not shown because too many files have changed in this diff Show More

@ -46,7 +46,7 @@
android:name=".help.permission.PermissionActivity"
android:theme="@style/Activity.Permission" />
<activity
android:name=".ui.book.source.edit.SourceEditActivity"
android:name=".ui.book.source.edit.BookSourceEditActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" />
@ -78,7 +78,8 @@
<activity android:name=".ui.audio.AudioPlayActivity" />
<activity android:name=".ui.explore.ExploreShowActivity" />
<activity android:name=".ui.rss.source.manage.RssSourceActivity" />
<activity android:name=".ui.rss.source.debug.RssSourceDebugActivity" />
<activity android:name=".ui.rss.article.RssArticlesActivity"/>
<service android:name=".service.CheckSourceService" />
<service android:name=".service.DownloadService" />
<service android:name=".service.UpdateService" />

@ -2,26 +2,38 @@ package io.legado.app.base
import android.app.Application
import android.content.Context
import androidx.annotation.CallSuper
import androidx.lifecycle.AndroidViewModel
import io.legado.app.App
import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.*
import org.jetbrains.anko.AnkoLogger
import org.jetbrains.anko.toast
import kotlin.coroutines.CoroutineContext
open class BaseViewModel(application: Application) : AndroidViewModel(application), CoroutineScope by MainScope(),
open class BaseViewModel(application: Application) : AndroidViewModel(application),
CoroutineScope by MainScope(),
AnkoLogger {
val context: Context by lazy { this.getApplication<App>() }
fun <T> execute(scope: CoroutineScope = this, block: suspend CoroutineScope.() -> T): Coroutine<T> {
return Coroutine.async(scope) { block() }
fun <T> execute(
scope: CoroutineScope = this,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T
): Coroutine<T> {
return Coroutine.async(scope, context) { block() }
}
fun <R> submit(scope: CoroutineScope = this, block: suspend CoroutineScope.() -> Deferred<R>): Coroutine<R> {
return Coroutine.async(scope) { block().await() }
fun <R> submit(
scope: CoroutineScope = this,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> Deferred<R>
): Coroutine<R> {
return Coroutine.async(scope, context) { block().await() }
}
@CallSuper
override fun onCleared() {
super.onCleared()
cancel()

@ -38,11 +38,7 @@ object AppConst {
)
}
val defaultBookGroups by lazy {
listOf(
BookGroup(-1, "全部"),
BookGroup(-2, "本地"),
BookGroup(-3, "音频")
)
}
val bookGroupAll = BookGroup(-1, App.INSTANCE.getString(R.string.all))
val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local))
val bookGroupAudio = BookGroup(-3, App.INSTANCE.getString(R.string.audio))
}

@ -5,7 +5,7 @@ object RSSKeywords {
const val RSS_ITEM = "item"
const val RSS_ITEM_TITLE = "title"
const val RSS_ITEM_LINK = "link"
const val RSS_ITEM_AUTHOR = "dc:creator"
const val RSS_ITEM_AUTHOR = "author"
const val RSS_ITEM_CATEGORY = "category"
const val RSS_ITEM_THUMBNAIL = "media:thumbnail"
const val RSS_ITEM_ENCLOSURE = "enclosure"

@ -12,7 +12,7 @@ import io.legado.app.data.entities.*
@Database(
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, ReplaceRule::class, SearchBook::class,
SearchKeyword::class, SourceCookie::class, RssSource::class, Bookmark::class],
SearchKeyword::class, SourceCookie::class, RssSource::class, Bookmark::class, RssArticle::class],
version = 1,
exportSchema = true
)
@ -58,4 +58,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun sourceCookieDao(): SourceCookieDao
abstract fun rssSourceDao(): RssSourceDao
abstract fun bookmarkDao(): BookmarkDao
abstract fun rssArtivleDao(): RssArticleDao
}

@ -24,6 +24,9 @@ interface BookDao {
@Query("SELECT bookUrl FROM books WHERE `group` = :group")
fun observeUrlsByGroup(group: Int): LiveData<List<String>>
@Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
fun liveDataSearch(key: String): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE `name` in (:names)")
fun findByName(vararg names: String): List<Book>

@ -1,10 +1,7 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.*
import io.legado.app.data.entities.BookGroup
@Dao
@ -17,5 +14,11 @@ interface BookGroupDao {
val maxId: Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(bookGroup: BookGroup)
fun insert(vararg bookGroup: BookGroup)
@Update
fun update(vararg bookGroup: BookGroup)
@Delete
fun delete(vararg bookGroup: BookGroup)
}

@ -11,7 +11,7 @@ interface BookSourceDao {
@Query("select * from book_sources order by customOrder asc")
fun liveDataAll(): LiveData<List<BookSource>>
@Query("select * from book_sources where bookSourceName like :searchKey or `bookSourceGroup` like :searchKey or bookSourceUrl like :searchKey order by customOrder asc")
@Query("select * from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey order by customOrder asc")
fun liveDataSearch(searchKey: String = ""): LiveData<List<BookSource>>
@Query("select * from book_sources where enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' order by customOrder asc")
@ -23,7 +23,7 @@ interface BookSourceDao {
@Query("select bookSourceGroup from book_sources where bookSourceGroup is not null and bookSourceGroup <> ''")
fun liveGroup(): LiveData<List<String>>
@Query("select distinct enabled from book_sources where bookSourceName like :searchKey or `bookSourceGroup` like :searchKey or bookSourceUrl like :searchKey")
@Query("select distinct enabled from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey")
fun searchIsEnable(searchKey: String = ""): List<Boolean>
@Query("update book_sources set enabled = 1 where bookSourceUrl in (:sourceUrls)")
@ -65,6 +65,12 @@ interface BookSourceDao {
@Delete
fun delete(vararg bookSource: BookSource)
@Query("delete from book_sources where bookSourceUrl = :key")
fun delete(key: String)
@get:Query("select min(customOrder) from book_sources")
val minOrder: Int
@get:Query("select max(customOrder) from book_sources")
val maxOrder: Int
}

@ -0,0 +1,18 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.RssArticle
@Dao
interface RssArticleDao {
@Query("select * from rssArticles where origin = :origin order by time desc")
fun liveByOrigin(origin: String): LiveData<List<RssArticle>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(vararg rssArticle: RssArticle)
@Update
fun update(vararg rssArticle: RssArticle)
}

@ -7,21 +7,48 @@ import io.legado.app.data.entities.RssSource
@Dao
interface RssSourceDao {
@Query("select * from rssSources where sourceUrl = :key")
fun getByKey(key: String): RssSource?
@get:Query("SELECT * FROM rssSources")
val all: List<RssSource>
@Query("SELECT * FROM rssSources")
@Query("SELECT * FROM rssSources order by customOrder")
fun liveAll(): LiveData<List<RssSource>>
@Query("SELECT * FROM rssSources where enabled = 1")
@Query("SELECT * FROM rssSources where sourceName like :key or sourceUrl like :key or sourceGroup like :key order by customOrder")
fun liveSearch(key: String): LiveData<List<RssSource>>
@Query("SELECT * FROM rssSources where enabled = 1 order by customOrder")
fun liveEnabled(): LiveData<List<RssSource>>
@Query("select sourceGroup from rssSources where sourceGroup is not null and sourceGroup <> ''")
fun liveGroup(): LiveData<List<String>>
@Query("update rssSources set enabled = 1 where sourceUrl in (:sourceUrls)")
fun enableSection(vararg sourceUrls: String)
@Query("update rssSources set enabled = 0 where sourceUrl in (:sourceUrls)")
fun disableSection(vararg sourceUrls: String)
@get:Query("select min(customOrder) from rssSources")
val minOrder: Int
@get:Query("select max(customOrder) from rssSources")
val maxOrder: Int
@Query("delete from rssSources where sourceUrl in (:sourceUrls)")
fun delSection(vararg sourceUrls: String)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rssSource: RssSource)
@Update
fun update(vararg rssSource: RssSource)
@Delete
fun delete(vararg rssSource: RssSource)
@Query("delete from rssSources where sourceUrl = :sourceUrl")
fun delete(sourceUrl: String)
}

@ -1,6 +1,6 @@
package io.legado.app.data.dao
import androidx.paging.DataSource
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.SearchKeyword
@ -9,16 +9,19 @@ import io.legado.app.data.entities.SearchKeyword
interface SearchKeywordDao {
@Query("SELECT * FROM search_keywords ORDER BY usage DESC")
fun observeByUsage(): DataSource.Factory<Int, SearchKeyword>
fun liveDataByUsage(): LiveData<List<SearchKeyword>>
@Query("SELECT * FROM search_keywords ORDER BY lastUseTime DESC")
fun observeByTime(): DataSource.Factory<Int, SearchKeyword>
fun liveDataByTime(): LiveData<List<SearchKeyword>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg keywords: SearchKeyword)
@Query("SELECT * FROM search_keywords where word like '%'||:key||'%' ORDER BY usage DESC")
fun liveDataSearch(key: String): LiveData<List<SearchKeyword>>
@Query("select * from search_keywords where word = :key")
fun get(key: String): SearchKeyword?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(keyword: SearchKeyword): Long
fun insert(vararg keywords: SearchKeyword)
@Update
fun update(vararg keywords: SearchKeyword)

@ -0,0 +1,3 @@
package io.legado.app.data.entities
data class EditEntity(var key: String, var value: String?, var hint: Int)

@ -1,14 +1,16 @@
package io.legado.app.data.entities
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
@Entity(tableName = "rssArticles")
data class RssArticle(
var origin: String = "",
var time: Long = System.currentTimeMillis(),
@PrimaryKey
var guid: String? = null,
var guid: String = "",
var title: String? = null,
var author: String? = null,
var link: String? = null,
@ -16,5 +18,12 @@ data class RssArticle(
var description: String? = null,
var content: String? = null,
var image: String? = null,
var categories: MutableList<String> = mutableListOf()
)
var categories: String? = null,
var read: Boolean = false,
var star: Boolean = false
) {
@Ignore
var categoryList: MutableList<String> = mutableListOf()
}

@ -2,25 +2,31 @@ package io.legado.app.data.entities
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize
@Parcelize
@Entity(tableName = "rssSources")
@Entity(tableName = "rssSources", indices = [(Index(value = ["sourceUrl"], unique = false))])
data class RssSource(
var sourceName: String,
@PrimaryKey
var sourceUrl: String,
var iconUrl: String,
var sourceUrl: String = "",
var sourceName: String = "",
var sourceIcon: String = "",
var sourceGroup: String? = null,
var enabled: Boolean = true,
//列表规则
var ruleArticles: String? = null,
var ruleGuid: String? = null,
var ruleTitle: String? = null,
var ruleAuthor: String? = null,
var ruleLink: String? = null,
var rulePubDate: String? = null,
//类别
var ruleCategories: String? = null,
//描述
var ruleDescription: String? = null,
var ruleContent: String? = null,
var ruleImage: String? = null,
var ruleCategories: String? = null,
var ruleContent: String? = null,
var ruleLink: String? = null,
var customOrder: Int = 0
) : Parcelable

@ -13,5 +13,5 @@ data class SearchKeyword(
@PrimaryKey
var word: String = "", // 搜索关键词
var usage: Int = 1, // 使用次数
var lastUseTime: Long = 0 // 最后一次使用时间
var lastUseTime: Long = System.currentTimeMillis() // 最后一次使用时间
) : Parcelable

@ -1,6 +1,7 @@
package io.legado.app.help
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import io.legado.app.App
@ -52,7 +53,11 @@ object ReadBookConfig {
}
fun upBg() {
bg = getConfig().bgDrawable()
val resources = App.INSTANCE.resources
val dm = resources.displayMetrics
val width = dm.widthPixels
val height = dm.heightPixels
bg = getConfig().bgDrawable(width, height)
}
fun save() {
@ -162,20 +167,30 @@ object ReadBookConfig {
else bgType
}
fun bgDrawable(): Drawable {
fun bgDrawable(width: Int, height: Int): Drawable {
var bgDrawable: Drawable? = null
kotlin.runCatching {
when (bgType()) {
0 -> bgDrawable = ColorDrawable(Color.parseColor(bgStr()))
1 -> bgDrawable =
Drawable.createFromStream(
App.INSTANCE.assets.open("bg" + File.separator + bgStr()),
"bg"
val resources = App.INSTANCE.resources
try {
bgDrawable = when (bgType()) {
0 -> ColorDrawable(Color.parseColor(bgStr()))
1 -> {
BitmapDrawable(
resources,
BitmapUtil.decodeBitmap(
App.INSTANCE,
"bg" + File.separator + bgStr(),
width,
height
)
)
else -> runCatching {
bgDrawable = Drawable.createFromPath(bgStr())
}
else -> BitmapDrawable(
resources,
BitmapUtil.decodeBitmap(bgStr(), width, height)
)
}
} catch (e: Exception) {
e.printStackTrace()
}
return bgDrawable ?: ColorDrawable(App.INSTANCE.getCompatColor(R.color.background))
}

@ -1,18 +1,26 @@
package io.legado.app.help.coroutine
import android.util.Log
import io.legado.app.BuildConfig
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
class Coroutine<T>(scope: CoroutineScope, block: suspend CoroutineScope.() -> T) {
class Coroutine<T>(
scope: CoroutineScope,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T
) {
companion object {
val DEFAULT = MainScope()
fun <T> async(scope: CoroutineScope = DEFAULT, block: suspend CoroutineScope.() -> T): Coroutine<T> {
return Coroutine(scope, block)
fun <T> async(
scope: CoroutineScope = DEFAULT,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T
): Coroutine<T> {
return Coroutine(scope, context, block)
}
}
@ -37,7 +45,7 @@ class Coroutine<T>(scope: CoroutineScope, block: suspend CoroutineScope.() -> T)
get() = job.isCompleted
init {
this.job = executeInternal(scope, block)
this.job = executeInternal(scope, context, block)
}
fun timeout(timeMillis: () -> Long): Coroutine<T> {
@ -101,13 +109,20 @@ class Coroutine<T>(scope: CoroutineScope, block: suspend CoroutineScope.() -> T)
return job.invokeOnCompletion(handler)
}
private fun executeInternal(scope: CoroutineScope, block: suspend CoroutineScope.() -> T): Job {
private fun executeInternal(
scope: CoroutineScope,
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): Job {
return scope.plus(Dispatchers.Main).launch {
try {
start?.let { dispatchVoidCallback(this, it) }
val value = executeBlock(scope, timeMillis ?: 0L, block)
val value = executeBlock(scope, context, timeMillis ?: 0L, block)
success?.let { dispatchCallback(this, value, it) }
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
val consume: Boolean = errorReturn?.value?.let { value ->
success?.let { dispatchCallback(this, value, it) }
true
@ -148,10 +163,11 @@ class Coroutine<T>(scope: CoroutineScope, block: suspend CoroutineScope.() -> T)
private suspend inline fun executeBlock(
scope: CoroutineScope,
context: CoroutineContext,
timeMillis: Long,
noinline block: suspend CoroutineScope.() -> T
): T? {
return withContext(scope.coroutineContext.plus(Dispatchers.IO)) {
return withContext(scope.coroutineContext.plus(context)) {
if (timeMillis > 0L) withTimeout(timeMillis) {
block()
} else block()

@ -35,7 +35,7 @@ object Backup {
fun autoBackup() {
doAsync {
val path = defaultPath + File.separator + "autoBackup"
val path = defaultPath
backupBookshelf(path)
backupBookSource(path)
backupRssSource(path)
@ -45,28 +45,44 @@ object Backup {
}
private fun backupBookshelf(path: String) {
val json = GSON.toJson(App.db.bookDao().allBooks)
App.db.bookDao().allBooks.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
val file = FileHelp.getFile(path + File.separator + "bookshelf.json")
file.writeText(json)
}
}
}
private fun backupBookSource(path: String) {
val json = GSON.toJson(App.db.bookSourceDao().all)
App.db.bookSourceDao().all.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
val file = FileHelp.getFile(path + File.separator + "bookSource.json")
file.writeText(json)
}
}
}
private fun backupRssSource(path: String) {
val json = GSON.toJson(App.db.rssSourceDao().all)
App.db.rssSourceDao().all.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
val file = FileHelp.getFile(path + File.separator + "rssSource.json")
file.writeText(json)
}
}
}
private fun backupReplaceRule(path: String) {
val json = GSON.toJson(App.db.replaceRuleDao().all)
App.db.replaceRuleDao().all.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
val file = FileHelp.getFile(path + File.separator + "replaceRule.json")
file.writeText(json)
}
}
}
private fun backupPreference(path: String) {
Preferences.getSharedPreferences(App.INSTANCE, path, "config")?.let { sp ->

@ -39,8 +39,8 @@ object WebDavHelp {
val url = getWebDavUrl()
val names = arrayListOf<String>()
if (!url.isNullOrBlank() && initWebDav()) {
val files = WebDav(url + "legado/").listFiles()
files.reversed()
var files = WebDav(url + "legado/").listFiles()
files = files.reversed()
for (index: Int in 0 until min(10, files.size)) {
files[index].displayName?.let {
names.add(it)

@ -0,0 +1,24 @@
package io.legado.app.model
import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssSource
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.rss.RssParserByRule
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext
object Rss {
fun getArticles(
rssSource: RssSource,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<MutableList<RssArticle>> {
return Coroutine.async(scope, context) {
val response = AnalyzeUrl(rssSource.sourceUrl).getResponseAsync().await()
RssParserByRule.parseXML(response, rssSource)
}
}
}

@ -11,6 +11,8 @@ import io.legado.app.model.webbook.BookContent
import io.legado.app.model.webbook.BookInfo
import io.legado.app.model.webbook.BookList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext
class WebBook(val bookSource: BookSource) {
@ -20,9 +22,13 @@ class WebBook(val bookSource: BookSource) {
/**
* 搜索
*/
fun searchBook(key: String, page: Int? = 1, scope: CoroutineScope = Coroutine.DEFAULT)
: Coroutine<List<SearchBook>> {
return Coroutine.async(scope) {
fun searchBook(
key: String,
page: Int? = 1,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<List<SearchBook>> {
return Coroutine.async(scope, context) {
bookSource.searchUrl?.let { searchUrl ->
val analyzeUrl = AnalyzeUrl(
ruleUrl = searchUrl,
@ -40,9 +46,13 @@ class WebBook(val bookSource: BookSource) {
/**
* 发现
*/
fun exploreBook(url: String, page: Int? = 1, scope: CoroutineScope = Coroutine.DEFAULT)
: Coroutine<List<SearchBook>> {
return Coroutine.async(scope) {
fun exploreBook(
url: String,
page: Int? = 1,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<List<SearchBook>> {
return Coroutine.async(scope, context) {
val analyzeUrl = AnalyzeUrl(
ruleUrl = url,
page = page,
@ -57,8 +67,12 @@ class WebBook(val bookSource: BookSource) {
/**
* 书籍信息
*/
fun getBookInfo(book: Book, scope: CoroutineScope = Coroutine.DEFAULT): Coroutine<Book> {
return Coroutine.async(scope) {
fun getBookInfo(
book: Book,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<Book> {
return Coroutine.async(scope, context) {
val analyzeUrl = AnalyzeUrl(
book = book,
ruleUrl = book.bookUrl,
@ -76,9 +90,10 @@ class WebBook(val bookSource: BookSource) {
*/
fun getChapterList(
book: Book,
scope: CoroutineScope = Coroutine.DEFAULT
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<List<BookChapter>> {
return Coroutine.async(scope) {
return Coroutine.async(scope, context) {
val analyzeUrl = AnalyzeUrl(
book = book,
ruleUrl = book.tocUrl,
@ -97,9 +112,10 @@ class WebBook(val bookSource: BookSource) {
book: Book,
bookChapter: BookChapter,
nextChapterUrl: String? = null,
scope: CoroutineScope = Coroutine.DEFAULT
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<String> {
return Coroutine.async(scope) {
return Coroutine.async(scope, context) {
val analyzeUrl =
AnalyzeUrl(
book = book,

@ -445,7 +445,7 @@ class AnalyzeRule(private var book: BaseBook? = null) {
}
//分离正则表达式
val ruleStrS =
rule.trim { it <= ' ' }.splitNotBlank("##")
rule.trim { it <= ' ' }.split("##")
rule = ruleStrS[0]
if (ruleStrS.size > 1) {
replaceRegex = ruleStrS[1]

@ -11,7 +11,7 @@ import java.io.StringReader
object RssParser {
@Throws(XmlPullParserException::class, IOException::class)
fun parseXML(xml: String): MutableList<RssArticle> {
fun parseXML(xml: String, sourceUrl: String): MutableList<RssArticle> {
val articleList = mutableListOf<RssArticle>()
var currentArticle = RssArticle()
@ -42,7 +42,7 @@ object RssParser {
xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_AUTHOR, true) ->
if (insideItem) currentArticle.author = xmlPullParser.nextText().trim()
xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_CATEGORY, true) ->
if (insideItem) currentArticle.categories.add(xmlPullParser.nextText().trim())
if (insideItem) currentArticle.categoryList.add(xmlPullParser.nextText().trim())
xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_THUMBNAIL, true) ->
if (insideItem) currentArticle.image =
xmlPullParser.getAttributeValue(null, RSSKeywords.RSS_ITEM_URL)
@ -92,6 +92,8 @@ object RssParser {
) {
// The item is correctly parsed
insideItem = false
currentArticle.categories = currentArticle.categoryList.joinToString(",")
currentArticle.origin = sourceUrl
articleList.add(currentArticle)
currentArticle = RssArticle()
}

@ -0,0 +1,94 @@
package io.legado.app.model.rss
import io.legado.app.App
import io.legado.app.R
import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssSource
import io.legado.app.model.analyzeRule.AnalyzeRule
import retrofit2.Response
object RssParserByRule {
@Throws(Exception::class)
fun parseXML(response: Response<String>, rssSource: RssSource): MutableList<RssArticle> {
val articleList = mutableListOf<RssArticle>()
val xml = response.body()
if (xml.isNullOrBlank()) {
throw Exception(
App.INSTANCE.getString(
R.string.error_get_web_content,
rssSource.sourceUrl
)
)
}
rssSource.ruleArticles?.let { ruleArticles ->
val analyzeRule = AnalyzeRule()
analyzeRule.setContent(xml)
val collections = analyzeRule.getElements(ruleArticles)
val ruleGuid = analyzeRule.splitSourceRule(rssSource.ruleGuid ?: "")
val ruleTitle = analyzeRule.splitSourceRule(rssSource.ruleTitle ?: "")
val ruleAuthor = analyzeRule.splitSourceRule(rssSource.ruleAuthor ?: "")
val rulePubDate = analyzeRule.splitSourceRule(rssSource.rulePubDate ?: "")
val ruleCategories = analyzeRule.splitSourceRule(rssSource.ruleCategories ?: "")
val ruleDescription = analyzeRule.splitSourceRule(rssSource.ruleDescription ?: "")
val ruleImage = analyzeRule.splitSourceRule(rssSource.ruleImage ?: "")
val ruleContent = analyzeRule.splitSourceRule(rssSource.ruleContent ?: "")
val ruleLink = analyzeRule.splitSourceRule(rssSource.ruleLink ?: "")
for ((index, item) in collections.withIndex()) {
getItem(
item,
analyzeRule,
index == 0,
ruleGuid,
ruleTitle,
ruleAuthor,
rulePubDate,
ruleCategories,
ruleDescription,
ruleImage,
ruleContent,
ruleLink
)?.let {
it.origin = rssSource.sourceUrl
articleList.add(it)
}
}
} ?: let {
return RssParser.parseXML(xml, rssSource.sourceUrl)
}
return articleList
}
private fun getItem(
item: Any,
analyzeRule: AnalyzeRule,
printLog: Boolean,
ruleGuid: List<AnalyzeRule.SourceRule>,
ruleTitle: List<AnalyzeRule.SourceRule>,
ruleAuthor: List<AnalyzeRule.SourceRule>,
rulePubDate: List<AnalyzeRule.SourceRule>,
ruleCategories: List<AnalyzeRule.SourceRule>,
ruleDescription: List<AnalyzeRule.SourceRule>,
ruleImage: List<AnalyzeRule.SourceRule>,
ruleContent: List<AnalyzeRule.SourceRule>,
ruleLink: List<AnalyzeRule.SourceRule>
): RssArticle? {
val rssArticle = RssArticle()
analyzeRule.setContent(item)
rssArticle.guid = analyzeRule.getString(ruleGuid)
rssArticle.title = analyzeRule.getString(ruleTitle)
rssArticle.author = analyzeRule.getString(ruleAuthor)
rssArticle.pubDate = analyzeRule.getString(rulePubDate)
rssArticle.categories = analyzeRule.getString(ruleCategories)
rssArticle.description = analyzeRule.getString(ruleDescription)
rssArticle.image = analyzeRule.getString(ruleImage)
rssArticle.link = analyzeRule.getString(ruleLink)
rssArticle.content = analyzeRule.getString(ruleContent)
if (rssArticle.title.isNullOrBlank()) {
return null
}
return rssArticle
}
}

@ -7,6 +7,7 @@ import android.view.KeyEvent
import io.legado.app.constant.Bus
import io.legado.app.help.ActivityHelp
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.utils.LogUtils
import io.legado.app.utils.postEvent
@ -22,7 +23,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
fun handleIntent(context: Context, intent: Intent): Boolean {
val intentAction = intent.action
val keyEventAction = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.action
// LogUtils.log("耳机按键: $intentAction $keyEventAction")
LogUtils.d("耳机按键", "$intentAction $keyEventAction")
if (Intent.ACTION_MEDIA_BUTTON == intentAction) {
if (keyEventAction == KeyEvent.ACTION_DOWN) {
readAloud(context)

@ -24,7 +24,7 @@ class AboutFragment : PreferenceFragmentCompat() {
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
when (preference?.key) {
"mail" -> openIntent(Intent.ACTION_SENDTO, "mailto:kunfei.ge@gmail.com")
"gitHub" -> openIntent(Intent.ACTION_VIEW, getString(R.string.this_github_url))
"git" -> openIntent(Intent.ACTION_VIEW, getString(R.string.this_github_url))
}
return super.onPreferenceTreeClick(preference)
}

@ -8,11 +8,10 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import io.legado.app.R
import io.legado.app.base.BaseActivity
import io.legado.app.lib.theme.ATH
import io.legado.app.utils.getViewModel
import io.legado.app.utils.ACache
import kotlinx.android.synthetic.main.activity_donate.*
import kotlinx.android.synthetic.main.view_title_bar.*
import org.jetbrains.anko.toast
@ -60,7 +59,7 @@ class DonateActivity : BaseActivity(R.layout.activity_donate) {
} catch (e: Exception) {
e.printStackTrace()
} finally {
ACache.get(this, cacheDir = false).put("proTime", System.currentTimeMillis())
}
}

@ -14,7 +14,7 @@ import io.legado.app.help.ImageLoader
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.book.info.edit.BookInfoEditActivity
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.source.edit.SourceEditActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.changesource.ChangeSourceDialog
import io.legado.app.utils.getCompatDrawable
import io.legado.app.utils.getViewModel
@ -88,8 +88,8 @@ class BookInfoActivity : VMBaseActivity<BookInfoViewModel>(R.layout.activity_boo
book.getDisplayIntro() // getString(R.string.intro_show, book.getDisplayIntro())
book.getDisplayCover()?.let {
ImageLoader.load(this, it)
.placeholder(R.drawable.img_cover_default)
.error(R.drawable.img_cover_default)
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.centerCrop()
.setAsDrawable(iv_cover)
}
@ -183,10 +183,14 @@ class BookInfoActivity : VMBaseActivity<BookInfoViewModel>(R.layout.activity_boo
}
}
}
tv_loading.onClick { }
tv_loading.onClick {
viewModel.bookData.value?.let {
viewModel.loadBookInfo(it)
}
}
tv_origin.onClick {
viewModel.bookData.value?.let {
startActivity<SourceEditActivity>(Pair("data", it.origin))
startActivity<BookSourceEditActivity>(Pair("data", it.origin))
}
}
tv_change_source.onClick {
@ -204,8 +208,10 @@ class BookInfoActivity : VMBaseActivity<BookInfoViewModel>(R.layout.activity_boo
}
}
iv_chapter_top.onClick {
adapter.reorder = !adapter.reorder
adapter.notifyDataSetChanged()
rv_chapter_list.scrollToPosition(0)
}
iv_chapter_bottom.onClick {
rv_chapter_list.scrollToPosition(adapter.itemCount - 1)
}
}
@ -226,13 +232,11 @@ class BookInfoActivity : VMBaseActivity<BookInfoViewModel>(R.layout.activity_boo
}
}
override fun curOrigin(): String? {
return viewModel.bookData.value?.origin
}
override val curOrigin: String?
get() = viewModel.bookData.value?.origin
override fun oldBook(): Book? {
return viewModel.bookData.value
}
override val oldBook: Book?
get() = viewModel.bookData.value
override fun changeTo(book: Book) {
upLoading(true)

@ -99,7 +99,6 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
}
}.onError {
toast(R.string.error_get_chapter_list)
it.printStackTrace()
}
} ?: toast(R.string.error_no_source)
}
@ -130,7 +129,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
chapters
)
book.durChapterTitle = chapters[book.durChapterIndex].title
App.db.bookDao().update(book)
App.db.bookDao().insert(book)
App.db.bookChapterDao().insert(*chapters.toTypedArray())
bookData.postValue(book)
chapterListData.postValue(chapters)

@ -32,16 +32,17 @@ import io.legado.app.service.BaseReadAloudService
import io.legado.app.ui.book.read.config.*
import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR
import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR
import io.legado.app.ui.book.source.edit.SourceEditActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.changesource.ChangeSourceDialog
import io.legado.app.ui.chapterlist.ChapterListActivity
import io.legado.app.ui.replacerule.ReplaceRuleActivity
import io.legado.app.ui.replacerule.edit.ReplaceEditDialog
import io.legado.app.ui.widget.page.ChapterProvider
import io.legado.app.ui.widget.page.PageView
import io.legado.app.ui.widget.page.TextChapter
import io.legado.app.ui.widget.page.delegate.PageDelegate
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_read_book.*
import kotlinx.android.synthetic.main.activity_book_read.*
import kotlinx.android.synthetic.main.view_book_page.*
import kotlinx.android.synthetic.main.view_read_menu.*
import kotlinx.android.synthetic.main.view_title_bar.*
@ -54,7 +55,7 @@ import org.jetbrains.anko.startActivity
import org.jetbrains.anko.startActivityForResult
import org.jetbrains.anko.toast
class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_read_book),
class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_book_read),
PageView.CallBack,
ReadMenu.CallBack,
ReadAloudDialog.CallBack,
@ -113,7 +114,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_rea
private fun initView() {
tv_chapter_name.onClick {
viewModel.webBook?.let {
startActivityForResult<SourceEditActivity>(
startActivityForResult<BookSourceEditActivity>(
requestCodeEditSource,
Pair("data", it.bookSource.bookSourceUrl)
)
@ -304,7 +305,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_rea
tv_chapter_url.text = it.url
tv_chapter_url.visible()
}
read_menu.upReadProgress(it.pageSize().minus(1), viewModel.durPageIndex)
seek_read_page.max = it.pageSize().minus(1)
tv_pre.isEnabled = viewModel.durChapterIndex != 0
tv_next.isEnabled = viewModel.durChapterIndex != viewModel.chapterSize - 1
curPageChanged()
@ -312,6 +313,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_rea
}
private fun curPageChanged() {
seek_read_page.progress = viewModel.durPageIndex
when (readAloudStatus) {
Status.PLAY -> readAloud()
Status.PAUSE -> {
@ -328,13 +330,11 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_rea
return viewModel.chapterSize
}
override fun curOrigin(): String? {
return viewModel.bookData.value?.origin
}
override val curOrigin: String?
get() = viewModel.bookData.value?.origin
override fun oldBook(): Book? {
return viewModel.bookData.value
}
override val oldBook: Book?
get() = viewModel.bookData.value
override fun changeTo(book: Book) {
viewModel.changeTo(book)
@ -608,7 +608,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_rea
}
}
observeEvent<String>(Bus.REPLACE) {
toast(it)
ReplaceEditDialog().show(supportFragmentManager, "replaceEditDialog")
}
}

@ -68,7 +68,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
}
}
saveRead(book)
}.onError { it.printStackTrace() }
}
}
private fun loadBookInfo(
@ -103,7 +103,6 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
}
}?.onError {
toast(R.string.error_load_toc)
it.printStackTrace()
} ?: autoChangeSource()
}
}

@ -95,11 +95,6 @@ class ReadMenu : FrameLayout {
}
}
fun upReadProgress(max: Int, dur: Int) {
seek_read_page.max = max
seek_read_page.progress = dur
}
private fun brightnessAuto(): Boolean {
return context.getPrefBoolean("brightnessAuto", true)
}

@ -20,7 +20,7 @@ import io.legado.app.ui.book.read.Help
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.widget.font.FontSelectDialog
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_read_book.*
import kotlinx.android.synthetic.main.activity_book_read.*
import kotlinx.android.synthetic.main.dialog_read_book_style.*
import org.jetbrains.anko.sdk27.listeners.onCheckedChange
import org.jetbrains.anko.sdk27.listeners.onClick
@ -249,7 +249,7 @@ class ReadStyleDialog : DialogFragment() {
ReadBookConfig.getConfig(i).apply {
when (bgType()) {
2 -> ImageLoader.load(requireContext(), bgStr()).centerCrop().setAsDrawable(iv)
else -> iv.setImageDrawable(bgDrawable())
else -> iv.setImageDrawable(bgDrawable(100, 150))
}
}
}

@ -0,0 +1,24 @@
package io.legado.app.ui.book.search
import android.content.Context
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.Book
import kotlinx.android.synthetic.main.item_text.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
class BookAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<Book>(context, R.layout.item_text) {
override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) {
with(holder.itemView) {
text_view.text = item.name
onClick { callBack.showBookInfo(item.bookUrl) }
}
}
interface CallBack {
fun showBookInfo(url: String)
}
}

@ -0,0 +1,42 @@
package io.legado.app.ui.book.search
import android.content.Context
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.SearchKeyword
import io.legado.app.ui.widget.anima.explosion_field.ExplosionField
import kotlinx.android.synthetic.main.item_text.view.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick
class HistoryKeyAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<SearchKeyword>(context, R.layout.item_text) {
override fun convert(holder: ItemViewHolder, item: SearchKeyword, payloads: MutableList<Any>) {
with(holder.itemView) {
text_view.text = item.word
onClick {
callBack.searchHistory(item.word)
}
onLongClick {
it?.let {
ExplosionField(context).explode(it, true)
}
GlobalScope.launch(IO) {
App.db.searchKeywordDao().delete(item)
}
true
}
}
}
interface CallBack {
fun searchHistory(key: String)
}
}

@ -7,32 +7,44 @@ import androidx.lifecycle.Observer
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.SearchKeyword
import io.legado.app.data.entities.SearchShow
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.utils.getViewModel
import io.legado.app.utils.gone
import io.legado.app.utils.invisible
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.activity_search.*
import kotlinx.android.synthetic.main.activity_book_search.*
import kotlinx.android.synthetic.main.view_search.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.startActivity
class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_search), SearchAdapter.CallBack {
class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_search),
BookAdapter.CallBack,
HistoryKeyAdapter.CallBack,
SearchAdapter.CallBack {
override val viewModel: SearchViewModel
get() = getViewModel(SearchViewModel::class.java)
private lateinit var adapter: SearchAdapter
private lateinit var bookAdapter: BookAdapter
private lateinit var historyKeyAdapter: HistoryKeyAdapter
private var searchBookData: LiveData<PagedList<SearchShow>>? = null
private var historyData: LiveData<List<SearchKeyword>>? = null
private var bookData: LiveData<List<Book>>? = null
override fun onActivityCreated(savedInstanceState: Bundle?) {
initRecyclerView()
initSearchView()
initOtherView()
initData()
intent.getStringExtra("key")?.let {
search_view.setQuery(it, true)
@ -49,6 +61,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_search)
override fun onQueryTextSubmit(query: String?): Boolean {
search_view.clearFocus()
query?.let {
viewModel.saveSearchKey(query)
viewModel.search(it, {
refresh_progress_bar.isAutoLoading = true
initData()
@ -63,19 +76,43 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_search)
override fun onQueryTextChange(newText: String?): Boolean {
if (newText.isNullOrBlank()) viewModel.stop()
upHistory(newText)
return false
}
})
fb_stop.onClick { viewModel.stop() }
search_view.setOnQueryTextFocusChangeListener { _, hasFocus ->
if (hasFocus) {
ll_history.visible()
} else {
ll_history.invisible()
}
}
}
private fun initRecyclerView() {
ATH.applyEdgeEffectColor(recycler_view)
ATH.applyEdgeEffectColor(rv_bookshelf_search)
ATH.applyEdgeEffectColor(rv_history_key)
bookAdapter = BookAdapter(this, this)
rv_bookshelf_search.layoutManager =
LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
rv_bookshelf_search.adapter = bookAdapter
historyKeyAdapter = HistoryKeyAdapter(this, this)
rv_history_key.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
rv_history_key.adapter = historyKeyAdapter
adapter = SearchAdapter(this)
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.adapter = adapter
}
private fun initOtherView() {
tv_clear_history.onClick { viewModel.clearHistory() }
fb_stop.onClick {
viewModel.stop()
refresh_progress_bar.isAutoLoading = false
}
}
private fun initData() {
searchBookData?.removeObservers(this)
searchBookData = LivePagedListBuilder(
@ -85,6 +122,35 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_search)
), 30
).build()
searchBookData?.observe(this, Observer { adapter.submitList(it) })
upHistory()
}
private fun upHistory(key: String? = null) {
bookData?.removeObservers(this)
if (key.isNullOrBlank()) {
tv_book_show.gone()
rv_bookshelf_search.gone()
} else {
bookData = App.db.bookDao().liveDataSearch(key)
bookData?.observe(this, Observer {
if (it.isEmpty()) {
tv_book_show.gone()
rv_bookshelf_search.gone()
} else {
tv_book_show.visible()
rv_bookshelf_search.visible()
}
bookAdapter.setItems(it)
})
}
historyData?.removeObservers(this)
historyData =
if (key.isNullOrBlank()) {
App.db.searchKeywordDao().liveDataByUsage()
} else {
App.db.searchKeywordDao().liveDataSearch(key)
}
historyData?.observe(this, Observer { historyKeyAdapter.setItems(it) })
}
override fun showBookInfo(name: String, author: String) {
@ -94,4 +160,12 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_search)
}
}
}
override fun showBookInfo(url: String) {
startActivity<BookInfoActivity>(Pair("bookUrl", url))
}
override fun searchHistory(key: String) {
search_view.setQuery(key, false)
}
}

@ -84,8 +84,8 @@ class SearchAdapter(val callBack: CallBack) :
}
searchBook.coverUrl.let {
ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.img_cover_default)
.error(R.drawable.img_cover_default)
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.centerCrop()
.setAsDrawable(iv_cover)
}
@ -100,8 +100,8 @@ class SearchAdapter(val callBack: CallBack) :
1 -> bv_originCount.setBadgeCount(searchBook.originCount)
2 -> searchBook.coverUrl.let {
ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.img_cover_default)
.error(R.drawable.img_cover_default)
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.centerCrop()
.setAsDrawable(iv_cover)
}

@ -4,12 +4,15 @@ import android.app.Application
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchKeyword
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
class SearchViewModel(application: Application) : BaseViewModel(application) {
private var searchPool = Executors.newFixedThreadPool(16).asCoroutineDispatcher()
private var task: Coroutine<*>? = null
var searchKey: String = ""
var startTime: Long = 0
@ -30,7 +33,12 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(key, searchPage, scope = this@execute)
WebBook(item).searchBook(
key,
searchPage,
scope = this@execute,
context = searchPool
)
.timeout(30000L)
.onSuccess(Dispatchers.IO) {
it?.let { list ->
@ -40,10 +48,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
}
}
}
delay(100)//每隔100毫秒搜索一个书源
}
}.onError {
it.printStackTrace()
}
task?.invokeOnCompletion {
@ -61,4 +66,24 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
success?.invoke(searchBook)
}
}
fun saveSearchKey(key: String) {
execute {
App.db.searchKeywordDao().get(key)?.let {
it.usage = it.usage + 1
App.db.searchKeywordDao().update(it)
} ?: App.db.searchKeywordDao().insert(SearchKeyword(key, 1))
}
}
fun clearHistory() {
execute {
App.db.searchKeywordDao().deleteAll()
}
}
override fun onCleared() {
super.onCleared()
searchPool.close()
}
}

@ -12,18 +12,19 @@ import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.qrcode.QrCodeActivity
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_book_source_debug.*
import kotlinx.android.synthetic.main.activity_source_debug.*
import kotlinx.android.synthetic.main.view_search.*
import kotlinx.coroutines.launch
import org.jetbrains.anko.startActivityForResult
import org.jetbrains.anko.toast
class SourceDebugActivity : VMBaseActivity<SourceDebugModel>(R.layout.activity_book_source_debug) {
class BookSourceDebugActivity :
VMBaseActivity<BookSourceDebugModel>(R.layout.activity_source_debug) {
override val viewModel: SourceDebugModel
get() = getViewModel(SourceDebugModel::class.java)
override val viewModel: BookSourceDebugModel
get() = getViewModel(BookSourceDebugModel::class.java)
private lateinit var adapter: SourceDebugAdapter
private lateinit var adapter: BookSourceDebugAdapter
private val qrRequestCode = 101
override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -42,7 +43,7 @@ class SourceDebugActivity : VMBaseActivity<SourceDebugModel>(R.layout.activity_b
private fun initRecyclerView() {
ATH.applyEdgeEffectColor(recycler_view)
adapter = SourceDebugAdapter(this)
adapter = BookSourceDebugAdapter(this)
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.adapter = adapter
rotate_loading.loadingColor = accentColor

@ -6,7 +6,7 @@ import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import kotlinx.android.synthetic.main.item_log.view.*
class SourceDebugAdapter(context: Context) :
class BookSourceDebugAdapter(context: Context) :
SimpleRecyclerAdapter<String>(context, R.layout.item_log) {
override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList<Any>) {
holder.itemView.apply {

@ -6,7 +6,8 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.model.WebBook
import io.legado.app.model.webbook.SourceDebug
class SourceDebugModel(application: Application) : BaseViewModel(application), SourceDebug.Callback {
class BookSourceDebugModel(application: Application) : BaseViewModel(application),
SourceDebug.Callback {
private var webBook: WebBook? = null

@ -0,0 +1,369 @@
package io.legado.app.ui.book.source.edit
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Rect
import android.os.Bundle
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.ViewTreeObserver
import android.widget.EditText
import android.widget.PopupWindow
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.tabs.TabLayout
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppConst
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.EditEntity
import io.legado.app.data.entities.rule.*
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.utils.GSON
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_book_source_edit.*
import org.jetbrains.anko.displayMetrics
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.toast
import kotlin.math.abs
class BookSourceEditActivity :
VMBaseActivity<BookSourceEditViewModel>(R.layout.activity_book_source_edit, false),
KeyboardToolPop.CallBack {
override val viewModel: BookSourceEditViewModel
get() = getViewModel(BookSourceEditViewModel::class.java)
private val adapter = BookSourceEditAdapter()
private val sourceEntities: ArrayList<EditEntity> = ArrayList()
private val searchEntities: ArrayList<EditEntity> = ArrayList()
private val findEntities: ArrayList<EditEntity> = ArrayList()
private val infoEntities: ArrayList<EditEntity> = ArrayList()
private val tocEntities: ArrayList<EditEntity> = ArrayList()
private val contentEntities: ArrayList<EditEntity> = ArrayList()
private var mSoftKeyboardTool: PopupWindow? = null
private var mIsSoftKeyBoardShowing = false
override fun onActivityCreated(savedInstanceState: Bundle?) {
initView()
viewModel.sourceLiveData.observe(this, Observer {
upRecyclerView(it)
})
viewModel.initData(intent)
}
override fun onDestroy() {
super.onDestroy()
mSoftKeyboardTool?.dismiss()
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.source_edit, menu)
return super.onCompatCreateOptionsMenu(menu)
}
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_save -> {
getSource()?.let {
viewModel.save(it) { setResult(Activity.RESULT_OK); finish() }
}
}
R.id.menu_debug_source -> {
getSource()?.let {
viewModel.save(it) {
startActivity<BookSourceDebugActivity>(Pair("key", it.bookSourceUrl))
}
}
}
R.id.menu_copy_source -> {
GSON.toJson(getSource())?.let { sourceStr ->
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
clipboard?.primaryClip = ClipData.newPlainText(null, sourceStr)
}
}
R.id.menu_paste_source -> viewModel.pasteSource()
}
return super.onCompatOptionsItemSelected(item)
}
private fun initView() {
ATH.applyEdgeEffectColor(recycler_view)
mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this)
window.decorView.viewTreeObserver.addOnGlobalLayoutListener(KeyboardOnGlobalChangeListener())
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.adapter = adapter
tab_layout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab?) {
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabSelected(tab: TabLayout.Tab?) {
setEditEntities(tab?.position)
}
})
}
private fun setEditEntities(tabPosition: Int?) {
when (tabPosition) {
1 -> adapter.editEntities = searchEntities
2 -> adapter.editEntities = findEntities
3 -> adapter.editEntities = infoEntities
4 -> adapter.editEntities = tocEntities
5 -> adapter.editEntities = contentEntities
else -> adapter.editEntities = sourceEntities
}
recycler_view.scrollToPosition(0)
}
private fun upRecyclerView(bookSource: BookSource?) {
bookSource?.let {
cb_is_enable.isChecked = it.enabled
cb_is_enable_find.isChecked = it.enabledExplore
}
//基本信息
sourceEntities.clear()
sourceEntities.apply {
add(EditEntity("bookSourceUrl", bookSource?.bookSourceUrl, R.string.book_source_url))
add(EditEntity("bookSourceName", bookSource?.bookSourceName, R.string.book_source_name))
add(
EditEntity(
"bookSourceGroup",
bookSource?.bookSourceGroup,
R.string.book_source_group
)
)
add(EditEntity("loginUrl", bookSource?.loginUrl, R.string.book_source_login_url))
add(EditEntity("bookUrlPattern", bookSource?.bookUrlPattern, R.string.book_url_pattern))
add(EditEntity("header", bookSource?.header, R.string.source_http_header))
}
//搜索
(bookSource?.getSearchRule()).let { searchRule ->
searchEntities.clear()
searchEntities.apply {
add(EditEntity("searchUrl", bookSource?.searchUrl, R.string.rule_search_url))
add(EditEntity("bookList", searchRule?.bookList, R.string.rule_book_list))
add(EditEntity("name", searchRule?.name, R.string.rule_book_name))
add(EditEntity("author", searchRule?.author, R.string.rule_book_author))
add(EditEntity("kind", searchRule?.kind, R.string.rule_book_kind))
add(EditEntity("wordCount", searchRule?.wordCount, R.string.rule_word_count))
add(EditEntity("lastChapter", searchRule?.lastChapter, R.string.rule_last_chapter))
add(EditEntity("intro", searchRule?.intro, R.string.rule_book_intro))
add(EditEntity("coverUrl", searchRule?.coverUrl, R.string.rule_cover_url))
add(EditEntity("bookUrl", searchRule?.bookUrl, R.string.rule_book_url))
}
}
//详情页
(bookSource?.getBookInfoRule()).let { infoRule ->
infoEntities.clear()
infoEntities.apply {
add(EditEntity("init", infoRule?.init, R.string.rule_book_info_init))
add(EditEntity("name", infoRule?.name, R.string.rule_book_name))
add(EditEntity("author", infoRule?.author, R.string.rule_book_author))
add(EditEntity("coverUrl", infoRule?.coverUrl, R.string.rule_cover_url))
add(EditEntity("intro", infoRule?.intro, R.string.rule_book_intro))
add(EditEntity("kind", infoRule?.kind, R.string.rule_book_kind))
add(EditEntity("wordCount", infoRule?.wordCount, R.string.rule_word_count))
add(EditEntity("lastChapter", infoRule?.lastChapter, R.string.rule_last_chapter))
add(EditEntity("tocUrl", infoRule?.tocUrl, R.string.rule_toc_url))
}
}
//目录页
(bookSource?.getTocRule()).let { tocRule ->
tocEntities.clear()
tocEntities.apply {
add(EditEntity("chapterList", tocRule?.chapterList, R.string.rule_chapter_list))
add(EditEntity("chapterName", tocRule?.chapterName, R.string.rule_chapter_name))
add(EditEntity("chapterUrl", tocRule?.chapterUrl, R.string.rule_chapter_url))
add(EditEntity("nextTocUrl", tocRule?.nextTocUrl, R.string.rule_next_toc_url))
}
}
//正文页
(bookSource?.getContentRule()).let { contentRule ->
contentEntities.clear()
contentEntities.apply {
add(EditEntity("content", contentRule?.content, R.string.rule_book_content))
add(
EditEntity(
"nextContentUrl",
contentRule?.nextContentUrl,
R.string.rule_content_url_next
)
)
}
}
//发现
(bookSource?.getExploreRule()).let { exploreRule ->
findEntities.clear()
findEntities.apply {
add(EditEntity("exploreUrl", bookSource?.exploreUrl, R.string.rule_find_url))
add(EditEntity("bookList", exploreRule?.bookList, R.string.rule_book_list))
add(EditEntity("name", exploreRule?.name, R.string.rule_book_name))
add(EditEntity("author", exploreRule?.author, R.string.rule_book_author))
add(EditEntity("kind", exploreRule?.kind, R.string.rule_book_kind))
add(EditEntity("wordCount", exploreRule?.wordCount, R.string.rule_word_count))
add(EditEntity("intro", exploreRule?.intro, R.string.rule_book_intro))
add(EditEntity("lastChapter", exploreRule?.lastChapter, R.string.rule_last_chapter))
add(EditEntity("coverUrl", exploreRule?.coverUrl, R.string.rule_cover_url))
add(EditEntity("bookUrl", exploreRule?.bookUrl, R.string.rule_book_url))
}
}
setEditEntities(0)
}
private fun getSource(): BookSource? {
val source = viewModel.sourceLiveData.value ?: BookSource()
source.enabled = cb_is_enable.isChecked
source.enabledExplore = cb_is_enable_find.isChecked
viewModel.sourceLiveData.value?.let {
source.customOrder = it.customOrder
source.weight = it.weight
}
val searchRule = SearchRule()
val exploreRule = ExploreRule()
val bookInfoRule = BookInfoRule()
val tocRule = TocRule()
val contentRule = ContentRule()
sourceEntities.forEach {
when (it.key) {
"bookSourceUrl" -> source.bookSourceUrl = it.value ?: ""
"bookSourceName" -> source.bookSourceName = it.value ?: ""
"bookSourceGroup" -> source.bookSourceGroup = it.value
"loginUrl" -> source.loginUrl = it.value
"bookUrlPattern" -> source.bookUrlPattern = it.value
"header" -> source.header = it.value
}
}
if (source.bookSourceUrl.isBlank() || source.bookSourceName.isBlank()) {
toast("书源名称和URL不能为空")
return null
}
searchEntities.forEach {
when (it.key) {
"searchUrl" -> source.searchUrl = it.value
"bookList" -> searchRule.bookList = it.value
"name" -> searchRule.name = it.value
"author" -> searchRule.author = it.value
"kind" -> searchRule.kind = it.value
"intro" -> searchRule.intro = it.value
"updateTime" -> searchRule.updateTime = it.value
"wordCount" -> searchRule.wordCount = it.value
"lastChapter" -> searchRule.lastChapter = it.value
"coverUrl" -> searchRule.coverUrl = it.value
"bookUrl" -> searchRule.bookUrl = it.value
}
}
findEntities.forEach {
when (it.key) {
"exploreUrl" -> source.exploreUrl = it.value
"bookList" -> exploreRule.bookList = it.value
"name" -> exploreRule.name = it.value
"author" -> exploreRule.author = it.value
"kind" -> exploreRule.kind = it.value
"intro" -> exploreRule.intro = it.value
"updateTime" -> exploreRule.updateTime = it.value
"wordCount" -> exploreRule.wordCount = it.value
"lastChapter" -> exploreRule.lastChapter = it.value
"coverUrl" -> exploreRule.coverUrl = it.value
"bookUrl" -> exploreRule.bookUrl = it.value
}
}
infoEntities.forEach {
when (it.key) {
"init" -> bookInfoRule.init = it.value
"name" -> bookInfoRule.name = it.value
"author" -> bookInfoRule.author = it.value
"kind" -> bookInfoRule.kind = it.value
"intro" -> bookInfoRule.intro = it.value
"updateTime" -> bookInfoRule.updateTime = it.value
"wordCount" -> bookInfoRule.wordCount = it.value
"lastChapter" -> bookInfoRule.lastChapter = it.value
"coverUrl" -> bookInfoRule.coverUrl = it.value
"tocUrl" -> bookInfoRule.tocUrl = it.value
}
}
tocEntities.forEach {
when (it.key) {
"chapterList" -> tocRule.chapterList = it.value
"chapterName" -> tocRule.chapterName = it.value
"chapterUrl" -> tocRule.chapterUrl = it.value
"nextTocUrl" -> tocRule.nextTocUrl = it.value
}
}
contentEntities.forEach {
when (it.key) {
"content" -> contentRule.content = it.value
"nextContentUrl" -> contentRule.nextContentUrl = it.value
}
}
source.ruleSearch = GSON.toJson(searchRule)
source.ruleExplore = GSON.toJson(exploreRule)
source.ruleBookInfo = GSON.toJson(bookInfoRule)
source.ruleToc = GSON.toJson(tocRule)
source.ruleContent = GSON.toJson(contentRule)
return source
}
override fun sendText(text: String) {
if (text.isBlank()) return
val view = window.decorView.findFocus()
if (view is EditText) {
val start = view.selectionStart
val end = view.selectionEnd
val edit = view.editableText//获取EditText的文字
if (start < 0 || start >= edit.length) {
edit.append(text)
} else {
edit.replace(start, end, text)//光标所在位置插入文字
}
}
}
private fun showKeyboardTopPopupWindow() {
mSoftKeyboardTool?.isShowing?.let { if (it) return }
if (!isFinishing) {
mSoftKeyboardTool?.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0)
}
}
private fun closePopupWindow() {
mSoftKeyboardTool?.let {
if (it.isShowing) {
it.dismiss()
}
}
}
private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
val rect = Rect()
// 获取当前页面窗口的显示范围
window.decorView.getWindowVisibleDisplayFrame(rect)
val screenHeight = this@BookSourceEditActivity.displayMetrics.heightPixels
val keyboardHeight = screenHeight - rect.bottom // 输入法的高度
val preShowing = mIsSoftKeyBoardShowing
if (abs(keyboardHeight) > screenHeight / 5) {
mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法
recycler_view.setPadding(0, 0, 0, 100)
showKeyboardTopPopupWindow()
} else {
mIsSoftKeyBoardShowing = false
recycler_view.setPadding(0, 0, 0, 0)
if (preShowing) {
closePopupWindow()
}
}
}
}
}

@ -7,11 +7,12 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.data.entities.EditEntity
import kotlinx.android.synthetic.main.item_source_edit.view.*
class SourceEditAdapter : RecyclerView.Adapter<SourceEditAdapter.MyViewHolder>() {
class BookSourceEditAdapter : RecyclerView.Adapter<BookSourceEditAdapter.MyViewHolder>() {
var editEntities: ArrayList<SourceEditActivity.EditEntity> = ArrayList()
var editEntities: ArrayList<EditEntity> = ArrayList()
set(value) {
field = value
notifyDataSetChanged()
@ -34,7 +35,7 @@ class SourceEditAdapter : RecyclerView.Adapter<SourceEditAdapter.MyViewHolder>()
}
class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(editEntity: SourceEditActivity.EditEntity) = with(itemView) {
fun bind(editEntity: EditEntity) = with(itemView) {
if (editText.getTag(R.id.tag1) == null) {
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {

@ -3,29 +3,44 @@ package io.legado.app.ui.book.source.edit
import android.app.Application
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import androidx.lifecycle.MutableLiveData
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.BookSource
import io.legado.app.help.storage.OldRule
class SourceEditViewModel(application: Application) : BaseViewModel(application) {
class BookSourceEditViewModel(application: Application) : BaseViewModel(application) {
val sourceLiveData: MutableLiveData<BookSource> = MutableLiveData()
var oldSourceUrl: String? = null
fun setBookSource(key: String) {
fun initData(intent: Intent) {
execute {
App.db.bookSourceDao().getBookSource(key)?.let {
val key = intent.getStringExtra("data")
var source: BookSource? = null
if (key != null) {
source = App.db.bookSourceDao().getBookSource(key)
}
source?.let {
oldSourceUrl = it.bookSourceUrl
sourceLiveData.postValue(it)
} ?: sourceLiveData.postValue(BookSource())
} ?: let {
sourceLiveData.postValue(BookSource().apply {
customOrder = App.db.bookSourceDao().maxOrder + 1
})
}
}
}
fun save(bookSource: BookSource, finally: (() -> Unit)? = null) {
execute {
if (bookSource.customOrder == 0) {
bookSource.customOrder = App.db.bookSourceDao().allCount()
oldSourceUrl?.let {
if (oldSourceUrl != bookSource.bookSourceUrl) {
App.db.bookSourceDao().delete(it)
}
}
oldSourceUrl = bookSource.bookSourceUrl
App.db.bookSourceDao().insert(bookSource)
}.onFinally {
finally?.let { it() }

@ -1,622 +0,0 @@
package io.legado.app.ui.book.source.edit
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Rect
import android.os.Bundle
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.ViewTreeObserver
import android.widget.EditText
import android.widget.PopupWindow
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.tabs.TabLayout
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppConst
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.rule.*
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.book.source.debug.SourceDebugActivity
import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.utils.GSON
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_book_source_edit.*
import org.jetbrains.anko.displayMetrics
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.toast
import kotlin.math.abs
class SourceEditActivity :
VMBaseActivity<SourceEditViewModel>(R.layout.activity_book_source_edit, false),
KeyboardToolPop.OnClickListener {
override val viewModel: SourceEditViewModel
get() = getViewModel(SourceEditViewModel::class.java)
private val adapter = SourceEditAdapter()
private val sourceEntities: ArrayList<EditEntity> = ArrayList()
private val searchEntities: ArrayList<EditEntity> = ArrayList()
private val findEntities: ArrayList<EditEntity> = ArrayList()
private val infoEntities: ArrayList<EditEntity> = ArrayList()
private val tocEntities: ArrayList<EditEntity> = ArrayList()
private val contentEntities: ArrayList<EditEntity> = ArrayList()
private var mSoftKeyboardTool: PopupWindow? = null
private var mIsSoftKeyBoardShowing = false
override fun onActivityCreated(savedInstanceState: Bundle?) {
initView()
viewModel.sourceLiveData.observe(this, Observer {
upRecyclerView(it)
})
if (viewModel.sourceLiveData.value == null) {
val sourceID = intent.getStringExtra("data")
if (sourceID == null) {
upRecyclerView(null)
} else {
sourceID.let { viewModel.setBookSource(sourceID) }
}
} else {
upRecyclerView(viewModel.sourceLiveData.value)
}
}
override fun onDestroy() {
super.onDestroy()
mSoftKeyboardTool?.dismiss()
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.source_edit, menu)
return super.onCompatCreateOptionsMenu(menu)
}
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_save -> {
val bookSource = getSource()
if (bookSource == null) {
toast("书源名称和URL不能为空")
} else {
viewModel.save(bookSource) { setResult(Activity.RESULT_OK); finish() }
}
}
R.id.menu_debug_source -> {
val bookSource = getSource()
if (bookSource == null) {
toast("书源名称和URL不能为空")
} else {
viewModel.save(bookSource) {
startActivity<SourceDebugActivity>("key" to bookSource.bookSourceUrl)
}
}
}
R.id.menu_copy_source -> {
GSON.toJson(getSource())?.let { sourceStr ->
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
clipboard?.primaryClip = ClipData.newPlainText(null, sourceStr)
}
}
R.id.menu_paste_source -> viewModel.pasteSource()
}
return super.onCompatOptionsItemSelected(item)
}
private fun initView() {
ATH.applyEdgeEffectColor(recycler_view)
mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this)
window.decorView.viewTreeObserver.addOnGlobalLayoutListener(KeyboardOnGlobalChangeListener())
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.adapter = adapter
tab_layout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab?) {
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabSelected(tab: TabLayout.Tab?) {
setEditEntities(tab?.position)
}
})
}
private fun setEditEntities(tabPosition: Int?) {
when (tabPosition) {
1 -> adapter.editEntities = searchEntities
2 -> adapter.editEntities = findEntities
3 -> adapter.editEntities = infoEntities
4 -> adapter.editEntities = tocEntities
5 -> adapter.editEntities = contentEntities
else -> adapter.editEntities = sourceEntities
}
recycler_view.scrollToPosition(0)
}
private fun upRecyclerView(bookSource: BookSource?) {
bookSource?.let {
cb_is_enable.isChecked = it.enabled
cb_is_enable_find.isChecked = it.enabledExplore
}
//基本信息
with(bookSource) {
sourceEntities.clear()
sourceEntities
.add(
EditEntity(
"bookSourceUrl",
this?.bookSourceUrl,
R.string.book_source_url
)
)
sourceEntities
.add(
EditEntity(
"bookSourceName",
this?.bookSourceName,
R.string.book_source_name
)
)
sourceEntities.add(
EditEntity(
"bookSourceGroup",
this?.bookSourceGroup,
R.string.book_source_group
)
)
sourceEntities
.add(
EditEntity(
"loginUrl",
this?.loginUrl,
R.string.book_source_login_url
)
)
sourceEntities
.add(
EditEntity(
"bookUrlPattern",
this?.bookUrlPattern,
R.string.book_url_pattern
)
)
sourceEntities.add(
EditEntity(
"header",
this?.header,
R.string.source_http_header
)
)
}
//搜索
with(bookSource?.getSearchRule()) {
searchEntities.clear()
searchEntities
.add(
EditEntity(
"searchUrl",
bookSource?.searchUrl,
R.string.rule_search_url
)
)
searchEntities.add(
EditEntity(
"bookList",
this?.bookList,
R.string.rule_book_list
)
)
searchEntities.add(
EditEntity(
"name",
this?.name,
R.string.rule_book_name
)
)
searchEntities.add(
EditEntity(
"author",
this?.author,
R.string.rule_book_author
)
)
searchEntities.add(
EditEntity(
"kind",
this?.kind,
R.string.rule_book_kind
)
)
searchEntities.add(
EditEntity(
"wordCount",
this?.wordCount,
R.string.rule_word_count
)
)
searchEntities
.add(
EditEntity(
"lastChapter",
this?.lastChapter,
R.string.rule_last_chapter
)
)
searchEntities.add(
EditEntity(
"intro",
this?.intro,
R.string.rule_book_intro
)
)
searchEntities.add(
EditEntity(
"coverUrl",
this?.coverUrl,
R.string.rule_cover_url
)
)
searchEntities.add(
EditEntity(
"bookUrl",
this?.bookUrl,
R.string.rule_book_url
)
)
}
//详情页
with(bookSource?.getBookInfoRule()) {
infoEntities.clear()
infoEntities.add(
EditEntity(
"init",
this?.init,
R.string.rule_book_info_init
)
)
infoEntities.add(
EditEntity(
"name",
this?.name,
R.string.rule_book_name
)
)
infoEntities.add(
EditEntity(
"author",
this?.author,
R.string.rule_book_author
)
)
infoEntities.add(
EditEntity(
"coverUrl",
this?.coverUrl,
R.string.rule_cover_url
)
)
infoEntities.add(
EditEntity(
"intro",
this?.intro,
R.string.rule_book_intro
)
)
infoEntities.add(
EditEntity(
"kind",
this?.kind,
R.string.rule_book_kind
)
)
infoEntities.add(
EditEntity(
"wordCount",
this?.wordCount,
R.string.rule_word_count
)
)
infoEntities.add(
EditEntity(
"lastChapter",
this?.lastChapter,
R.string.rule_last_chapter
)
)
infoEntities.add(
EditEntity(
"tocUrl",
this?.tocUrl,
R.string.rule_toc_url
)
)
}
//目录页
with(bookSource?.getTocRule()) {
tocEntities.clear()
tocEntities.add(
EditEntity(
"chapterList",
this?.chapterList,
R.string.rule_chapter_list
)
)
tocEntities.add(
EditEntity(
"chapterName",
this?.chapterName,
R.string.rule_chapter_name
)
)
tocEntities.add(
EditEntity(
"chapterUrl",
this?.chapterUrl,
R.string.rule_chapter_url
)
)
tocEntities.add(
EditEntity(
"nextTocUrl",
this?.nextTocUrl,
R.string.rule_next_toc_url
)
)
}
//正文页
with(bookSource?.getContentRule()) {
contentEntities.clear()
contentEntities.add(
EditEntity(
"content",
this?.content,
R.string.rule_book_content
)
)
contentEntities.add(
EditEntity(
"nextContentUrl",
this?.nextContentUrl,
R.string.rule_content_url_next
)
)
}
//发现
with(bookSource?.getExploreRule()) {
findEntities.clear()
findEntities.add(
EditEntity(
"exploreUrl",
bookSource?.exploreUrl,
R.string.rule_find_url
)
)
findEntities.add(
EditEntity(
"bookList",
this?.bookList,
R.string.rule_book_list
)
)
findEntities.add(
EditEntity(
"name",
this?.name,
R.string.rule_book_name
)
)
findEntities.add(
EditEntity(
"author",
this?.author,
R.string.rule_book_author
)
)
findEntities.add(
EditEntity(
"kind",
this?.kind,
R.string.rule_book_kind
)
)
findEntities.add(
EditEntity(
"wordCount",
this?.wordCount,
R.string.rule_word_count
)
)
findEntities.add(
EditEntity(
"intro",
this?.intro,
R.string.rule_book_intro
)
)
findEntities.add(
EditEntity(
"lastChapter",
this?.lastChapter,
R.string.rule_last_chapter
)
)
findEntities.add(
EditEntity(
"coverUrl",
this?.coverUrl,
R.string.rule_cover_url
)
)
findEntities.add(
EditEntity(
"bookUrl",
this?.bookUrl,
R.string.rule_book_url
)
)
}
setEditEntities(0)
}
private fun getSource(): BookSource? {
val source = viewModel.sourceLiveData.value ?: BookSource()
source.enabled = cb_is_enable.isChecked
source.enabledExplore = cb_is_enable_find.isChecked
viewModel.sourceLiveData.value?.let {
source.customOrder = it.customOrder
source.weight = it.weight
}
val searchRule = SearchRule()
val exploreRule = ExploreRule()
val bookInfoRule = BookInfoRule()
val tocRule = TocRule()
val contentRule = ContentRule()
for (entity in sourceEntities) {
with(entity) {
when (key) {
"bookSourceUrl" -> value?.let { source.bookSourceUrl = it } ?: return null
"bookSourceName" -> value?.let { source.bookSourceName = it } ?: return null
"bookSourceGroup" -> source.bookSourceGroup = value
"loginUrl" -> source.loginUrl = value
"bookUrlPattern" -> source.bookUrlPattern = value
"header" -> source.header = value
}
}
}
for (entity in searchEntities) {
with(entity) {
when (key) {
"searchUrl" -> source.searchUrl = value
"bookList" -> searchRule.bookList = value
"name" -> searchRule.name = value
"author" -> searchRule.author = value
"kind" -> searchRule.kind = value
"intro" -> searchRule.intro = value
"updateTime" -> searchRule.updateTime = value
"wordCount" -> searchRule.wordCount = value
"lastChapter" -> searchRule.lastChapter = value
"coverUrl" -> searchRule.coverUrl = value
"bookUrl" -> searchRule.bookUrl = value
}
}
}
for (entity in findEntities) {
with(entity) {
when (key) {
"exploreUrl" -> source.exploreUrl = value
"bookList" -> exploreRule.bookList = value
"name" -> exploreRule.name = value
"author" -> exploreRule.author = value
"kind" -> exploreRule.kind = value
"intro" -> exploreRule.intro = value
"updateTime" -> exploreRule.updateTime = value
"wordCount" -> exploreRule.wordCount = value
"lastChapter" -> exploreRule.lastChapter = value
"coverUrl" -> exploreRule.coverUrl = value
"bookUrl" -> exploreRule.bookUrl = value
}
}
}
for (entity in infoEntities) {
with(entity) {
when (key) {
"init" -> bookInfoRule.init = value
"name" -> bookInfoRule.name = value
"author" -> bookInfoRule.author = value
"kind" -> bookInfoRule.kind = value
"intro" -> bookInfoRule.intro = value
"updateTime" -> bookInfoRule.updateTime = value
"wordCount" -> bookInfoRule.wordCount = value
"lastChapter" -> bookInfoRule.lastChapter = value
"coverUrl" -> bookInfoRule.coverUrl = value
"tocUrl" -> bookInfoRule.tocUrl = value
}
}
}
for (entity in tocEntities) {
with(entity) {
when (key) {
"chapterList" -> tocRule.chapterList = value
"chapterName" -> tocRule.chapterName = value
"chapterUrl" -> tocRule.chapterUrl = value
"nextTocUrl" -> tocRule.nextTocUrl = value
}
}
}
for (entity in contentEntities) {
with(entity) {
when (key) {
"content" -> contentRule.content = value
"nextContentUrl" -> contentRule.nextContentUrl = value
}
}
}
source.ruleSearch = GSON.toJson(searchRule)
source.ruleExplore = GSON.toJson(exploreRule)
source.ruleBookInfo = GSON.toJson(bookInfoRule)
source.ruleToc = GSON.toJson(tocRule)
source.ruleContent = GSON.toJson(contentRule)
return source
}
override fun click(text: String) {
if (text.isBlank()) return
val view = window.decorView.findFocus()
if (view is EditText) {
val start = view.selectionStart
val end = view.selectionEnd
val edit = view.editableText//获取EditText的文字
if (start < 0 || start >= edit.length) {
edit.append(text)
} else {
edit.replace(start, end, text)//光标所在位置插入文字
}
}
}
private fun showKeyboardTopPopupWindow() {
mSoftKeyboardTool?.isShowing?.let { if (it) return }
if (!isFinishing) {
mSoftKeyboardTool?.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0)
}
}
private fun closePopupWindow() {
mSoftKeyboardTool?.let {
if (it.isShowing) {
it.dismiss()
}
}
}
private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
val rect = Rect()
// 获取当前页面窗口的显示范围
window.decorView.getWindowVisibleDisplayFrame(rect)
val screenHeight = this@SourceEditActivity.displayMetrics.heightPixels
val keyboardHeight = screenHeight - rect.bottom // 输入法的高度
val preShowing = mIsSoftKeyBoardShowing
if (abs(keyboardHeight) > screenHeight / 5) {
mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法
recycler_view.setPadding(0, 0, 0, 100)
showKeyboardTopPopupWindow()
} else {
mIsSoftKeyBoardShowing = false
recycler_view.setPadding(0, 0, 0, 0)
if (preShowing) {
closePopupWindow()
}
}
}
}
class EditEntity(var key: String, var value: String?, var hint: Int)
}

@ -28,7 +28,7 @@ import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.lib.theme.view.ATEAutoCompleteTextView
import io.legado.app.service.CheckSourceService
import io.legado.app.ui.book.source.edit.SourceEditActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.qrcode.QrCodeActivity
import io.legado.app.utils.ACache
import io.legado.app.utils.applyTint
@ -78,7 +78,7 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_add_book_source -> {
this.startActivity<SourceEditActivity>()
this.startActivity<BookSourceEditActivity>()
}
R.id.menu_import_book_source_qr -> {
this.startActivityForResult<QrCodeActivity>(qrRequestCode)
@ -151,12 +151,8 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
}
bookSourceLiveDate?.observe(this, Observer {
search_view.queryHint = getString(R.string.search_book_source_num, it.size)
val diffResult = DiffUtil.calculateDiff(
DiffCallBack(
adapter.getItems(),
it
)
)
val diffResult = DiffUtil
.calculateDiff(DiffCallBack(adapter.getItems(), it))
adapter.setItemsNoNotify(it)
diffResult.dispatchUpdatesTo(adapter)
})
@ -232,7 +228,7 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
}
override fun edit(bookSource: BookSource) {
startActivity<SourceEditActivity>(Pair("data", bookSource.bookSourceUrl))
startActivity<BookSourceEditActivity>(Pair("data", bookSource.bookSourceUrl))
}
override fun upOrder() {

@ -16,8 +16,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
fun topSource(bookSource: BookSource) {
execute {
val minXh = App.db.bookSourceDao().minOrder
bookSource.customOrder = minXh - 1
bookSource.customOrder = App.db.bookSourceDao().minOrder - 1
App.db.bookSourceDao().insert(bookSource)
}
}

@ -29,7 +29,6 @@ import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.splitNotBlank
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_book_group.view.tv_group
import kotlinx.android.synthetic.main.item_group_manage.view.*
import org.jetbrains.anko.sdk27.listeners.onClick

@ -35,6 +35,7 @@ class ChangeSourceDialog : DialogFragment(),
}
}
private var callBack: CallBack? = null
private lateinit var viewModel: ChangeSourceViewModel
private lateinit var changeSourceAdapter: ChangeSourceAdapter
@ -49,6 +50,7 @@ class ChangeSourceDialog : DialogFragment(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callBack = activity as? CallBack
viewModel.searchStateData.observe(viewLifecycleOwner, Observer {
refresh_progress_bar.isAutoLoading = it
})
@ -114,10 +116,8 @@ class ChangeSourceDialog : DialogFragment(),
}
override fun changeTo(searchBook: SearchBook) {
val activity = activity
if (activity is CallBack) {
val book = searchBook.toBook()
activity.oldBook()?.let { oldBook ->
callBack?.oldBook?.let { oldBook ->
book.durChapterIndex = oldBook.durChapterIndex
book.durChapterPos = oldBook.durChapterPos
book.durChapterTitle = oldBook.durChapterTitle
@ -127,18 +127,13 @@ class ChangeSourceDialog : DialogFragment(),
if (book.coverUrl.isNullOrEmpty()) {
book.coverUrl = oldBook.getDisplayCover()
}
activity.changeTo(book)
}
callBack?.changeTo(book)
}
dismiss()
}
override fun curOrigin(): String {
val activity = activity
if (activity is CallBack) {
return activity.curOrigin() ?: ""
}
return ""
return callBack?.curOrigin ?: ""
}
override fun adapter(): ChangeSourceAdapter {
@ -146,8 +141,8 @@ class ChangeSourceDialog : DialogFragment(),
}
interface CallBack {
fun curOrigin(): String?
fun oldBook(): Book?
val curOrigin: String?
val oldBook: Book?
fun changeTo(book: Book)
}

@ -12,11 +12,13 @@ import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.withContext
import org.jetbrains.anko.debug
import java.util.concurrent.Executors
class ChangeSourceViewModel(application: Application) : BaseViewModel(application) {
private var searchPool = Executors.newFixedThreadPool(16).asCoroutineDispatcher()
var callBack: CallBack? = null
val searchStateData = MutableLiveData<Boolean>()
var name: String = ""
@ -56,7 +58,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(name, scope = this@execute)
WebBook(item).searchBook(name, scope = this@execute, context = searchPool)
.timeout(30000L)
.onSuccess(IO) {
it?.forEach { searchBook ->
@ -70,7 +72,6 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
}
}
}
delay(100)
}
}
@ -130,4 +131,9 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
interface CallBack {
fun adapter(): ChangeSourceAdapter
}
override fun onCleared() {
super.onCleared()
searchPool.close()
}
}

@ -10,6 +10,7 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.help.BookHelp
import io.legado.app.lib.theme.ATH
import io.legado.app.utils.LogUtils
import io.legado.app.utils.getPrefString
@ -39,6 +40,7 @@ class ConfigFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChange
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
"downloadPath" -> BookHelp.upDownloadPath()
"recordLog" -> LogUtils.upLevel()
}
}

@ -59,8 +59,8 @@ class ExploreShowAdapter(context: Context, val callBack: CallBack) :
}
item.coverUrl.let {
ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.img_cover_default)
.error(R.drawable.img_cover_default)
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.centerCrop()
.setAsDrawable(iv_cover)
}

@ -39,8 +39,6 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application
App.db.searchBookDao().insert(*searchBooks.toTypedArray())
page++
}
}.onError {
it.printStackTrace()
}
}
}

@ -38,9 +38,6 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
App.db.bookChapterDao().insert(*it.toTypedArray())
}
}
.onError {
it.printStackTrace()
}
.onFinally {
synchronized(this) {
updateList.remove(book.bookUrl)

@ -35,7 +35,7 @@ class BooksAdapter(private val callBack: CallBack) :
}
fun notification(bookUrl: String) {
for (i in 0..itemCount) {
for (i in 0 until itemCount) {
getItem(i)?.let {
if (it.bookUrl == bookUrl) {
notifyItemChanged(i)
@ -72,8 +72,8 @@ class BooksAdapter(private val callBack: CallBack) :
tv_last.text = book.latestChapterTitle
book.getDisplayCover()?.let {
ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.img_cover_default)
.error(R.drawable.img_cover_default)
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.centerCrop()
.setAsDrawable(iv_cover)
}

@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseFragment
import io.legado.app.constant.Bus
import io.legado.app.data.entities.Book
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor
@ -20,6 +21,7 @@ import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.main.MainViewModel
import io.legado.app.utils.getViewModel
import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.observeEvent
import kotlinx.android.synthetic.main.fragment_books.*
import org.jetbrains.anko.startActivity
@ -52,6 +54,9 @@ class BooksFragment : VMBaseFragment<BooksViewModel>(R.layout.fragment_books),
}
initRecyclerView()
upRecyclerData()
observeEvent<String>(Bus.UP_BOOK) {
booksAdapter.notification(it)
}
}
private fun initRecyclerView() {
@ -70,7 +75,6 @@ class BooksFragment : VMBaseFragment<BooksViewModel>(R.layout.fragment_books),
})
booksAdapter = BooksAdapter(this)
rv_bookshelf.adapter = booksAdapter
}
private fun upRecyclerData() {

@ -1,11 +1,9 @@
package io.legado.app.ui.main.bookshelf
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
@ -16,11 +14,13 @@ import io.legado.app.R
import io.legado.app.base.VMBaseFragment
import io.legado.app.constant.AppConst
import io.legado.app.data.entities.BookGroup
import io.legado.app.lib.dialogs.*
import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getViewModel
import io.legado.app.utils.putPrefInt
import io.legado.app.utils.startActivity
import kotlinx.android.synthetic.main.fragment_bookshelf.*
import kotlinx.android.synthetic.main.view_tab_layout.*
import kotlinx.android.synthetic.main.view_title_bar.*
@ -28,13 +28,14 @@ import org.jetbrains.anko.startActivity
class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_bookshelf),
SearchView.OnQueryTextListener,
GroupManageDialog.CallBack,
BookshelfAdapter.CallBack {
override val viewModel: BookshelfViewModel
get() = getViewModel(BookshelfViewModel::class.java)
private var bookGroupLiveData: LiveData<List<BookGroup>>? = null
private val bookGroups = mutableListOf<BookGroup>().apply { addAll(AppConst.defaultBookGroups) }
private val bookGroups = mutableListOf<BookGroup>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setSupportToolbar(toolbar)
@ -51,6 +52,14 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
when (item.itemId) {
R.id.menu_search -> startActivity<SearchActivity>()
R.id.menu_bookshelf_layout -> selectBookshelfLayout()
R.id.menu_group_manage -> GroupManageDialog()
.show(childFragmentManager, "groupManageDialog")
R.id.menu_add_local -> {
}
R.id.menu_add_url -> {
}
R.id.menu_arrange_bookshelf -> {
}
}
}
@ -75,11 +84,18 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
bookGroupLiveData?.removeObservers(viewLifecycleOwner)
bookGroupLiveData = App.db.bookGroupDao().liveDataAll()
bookGroupLiveData?.observe(viewLifecycleOwner, Observer {
for (index in AppConst.defaultBookGroups.size until bookGroups.size) {
bookGroups.removeAt(AppConst.defaultBookGroups.size)
synchronized(this) {
bookGroups.clear()
bookGroups.add(AppConst.bookGroupAll)
if (getPrefBoolean("bookGroupLocal", true)) {
bookGroups.add(AppConst.bookGroupLocal)
}
if (getPrefBoolean("bookGroupAudio", true)) {
bookGroups.add(AppConst.bookGroupAudio)
}
bookGroups.addAll(it)
view_pager_bookshelf.adapter?.notifyDataSetChanged()
}
})
}
@ -92,22 +108,18 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
return false
}
@SuppressLint("InflateParams")
private fun showGroupInputDialog() {
alert(title = "新建分组") {
var editText: EditText? = null
customView {
layoutInflater.inflate(R.layout.dialog_edit_text, null).apply {
editText = edit_view.apply {
hint = "分组名称"
}
override fun upGroup() {
synchronized(this) {
bookGroups.remove(AppConst.bookGroupLocal)
bookGroups.remove(AppConst.bookGroupAudio)
if (getPrefBoolean("bookGroupAudio", true)) {
bookGroups.add(1, AppConst.bookGroupAudio)
}
if (getPrefBoolean("bookGroupLocal", true)) {
bookGroups.add(1, AppConst.bookGroupLocal)
}
yesButton {
viewModel.saveBookGroup(editText?.text?.toString())
view_pager_bookshelf.adapter?.notifyDataSetChanged()
}
noButton()
}.show().applyTint().requestInputMethod()
}
private fun selectBookshelfLayout() {

@ -7,17 +7,28 @@ import io.legado.app.data.entities.BookGroup
class BookshelfViewModel(application: Application) : BaseViewModel(application) {
fun saveBookGroup(group: String?) {
if (!group.isNullOrBlank()) {
fun addGroup(groupName: String) {
execute {
App.db.bookGroupDao().insert(
BookGroup(
App.db.bookGroupDao().maxId + 1,
group
)
val maxId = App.db.bookGroupDao().maxId
val bookGroup = BookGroup(
groupId = maxId.plus(1),
groupName = groupName,
order = maxId.plus(1)
)
App.db.bookGroupDao().insert(bookGroup)
}
}
fun upGroup(vararg bookGroup: BookGroup) {
execute {
App.db.bookGroupDao().update(*bookGroup)
}
}
fun delGroup(vararg bookGroup: BookGroup) {
execute {
App.db.bookGroupDao().delete(*bookGroup)
}
}
}

@ -0,0 +1,176 @@
package io.legado.app.ui.main.bookshelf
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.BookGroup
import io.legado.app.help.ItemTouchCallback
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_group_manage.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
private lateinit var viewModel: BookshelfViewModel
private lateinit var adapter: GroupAdapter
private var callBack: CallBack? = null
override fun onStart() {
super.onStart()
val dm = DisplayMetrics()
activity?.windowManager?.defaultDisplay?.getMetrics(dm)
dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt())
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = getViewModel(BookshelfViewModel::class.java)
return inflater.inflate(R.layout.dialog_recycler_view, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callBack = parentFragment as? CallBack
initData()
}
private fun initData() {
tool_bar.title = getString(R.string.group_manage)
tool_bar.inflateMenu(R.menu.book_group_manage)
tool_bar.menu.applyTint(requireContext(), false)
tool_bar.setOnMenuItemClickListener(this)
tool_bar.menu.findItem(R.id.menu_group_local)
.isChecked = getPrefBoolean("bookGroupLocal", true)
tool_bar.menu.findItem(R.id.menu_group_audio)
.isChecked = getPrefBoolean("bookGroupAudio", true)
adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(
DividerItemDecoration(requireContext(), RecyclerView.VERTICAL)
)
recycler_view.adapter = adapter
App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer {
adapter.setItems(it)
})
val itemTouchCallback = ItemTouchCallback()
itemTouchCallback.onItemTouchCallbackListener = adapter
itemTouchCallback.isCanDrag = true
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view)
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_add -> addGroup()
R.id.menu_group_local -> {
item.isChecked = !item.isChecked
putPrefBoolean("bookGroupLocal", item.isChecked)
callBack?.upGroup()
}
R.id.menu_group_audio -> {
item.isChecked = !item.isChecked
putPrefBoolean("bookGroupAudio", item.isChecked)
callBack?.upGroup()
}
}
return true
}
@SuppressLint("InflateParams")
private fun addGroup() {
alert(title = getString(R.string.add_group)) {
var editText: EditText? = null
customView {
layoutInflater.inflate(R.layout.dialog_edit_text, null).apply {
editText = edit_view.apply {
hint = "分组名称"
}
}
}
yesButton {
editText?.text?.toString()?.let {
if (it.isNotBlank()) {
viewModel.addGroup(it)
}
}
}
noButton()
}.show().applyTint().requestInputMethod()
}
@SuppressLint("InflateParams")
private fun editGroup(bookGroup: BookGroup) {
alert(title = getString(R.string.group_edit)) {
var editText: EditText? = null
customView {
layoutInflater.inflate(R.layout.dialog_edit_text, null).apply {
editText = edit_view.apply {
hint = "分组名称"
setText(bookGroup.groupName)
}
}
}
yesButton {
viewModel.upGroup(bookGroup.copy(groupName = editText?.text?.toString() ?: ""))
}
noButton()
}.show().applyTint().requestInputMethod()
}
private inner class GroupAdapter(context: Context) :
SimpleRecyclerAdapter<BookGroup>(context, R.layout.item_group_manage),
ItemTouchCallback.OnItemTouchCallbackListener {
override fun convert(holder: ItemViewHolder, item: BookGroup, payloads: MutableList<Any>) {
with(holder.itemView) {
tv_group.text = item.groupName
tv_edit.onClick { editGroup(item) }
tv_del.onClick { viewModel.delGroup(item) }
}
}
override fun onMove(srcPosition: Int, targetPosition: Int): Boolean {
val srcItem = getItem(srcPosition)
val targetItem = getItem(targetPosition)
if (srcItem != null && targetItem != null) {
val order = srcItem.order
srcItem.order = targetItem.order
targetItem.order = order
viewModel.upGroup(srcItem, targetItem)
}
return true
}
override fun onSwiped(adapterPosition: Int) {
}
}
interface CallBack {
fun upGroup()
}
}

@ -12,7 +12,7 @@ import io.legado.app.base.VMBaseFragment
import io.legado.app.data.entities.BookSource
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.ui.book.source.edit.SourceEditActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.explore.ExploreShowActivity
import io.legado.app.utils.getViewModel
import io.legado.app.utils.startActivity
@ -88,7 +88,7 @@ class ExploreFragment : VMBaseFragment<ExploreViewModel>(R.layout.fragment_find_
}
override fun editSource(sourceUrl: String) {
startActivity<SourceEditActivity>(Pair("data", sourceUrl))
startActivity<BookSourceEditActivity>(Pair("data", sourceUrl))
}
override fun toTop(source: BookSource) {

@ -10,6 +10,7 @@ import androidx.preference.PreferenceFragmentCompat
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseFragment
import io.legado.app.help.BookHelp
import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.help.storage.Backup
@ -21,6 +22,7 @@ import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.ui.config.ConfigActivity
import io.legado.app.ui.config.ConfigViewModel
import io.legado.app.ui.replacerule.ReplaceRuleActivity
import io.legado.app.utils.LogUtils
import io.legado.app.utils.startActivity
import kotlinx.android.synthetic.main.view_title_bar.*
import org.jetbrains.anko.startActivity
@ -82,9 +84,9 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
key: String?
) {
when (key) {
"isNightTheme" -> {
App.INSTANCE.applyDayNight()
}
"isNightTheme" -> App.INSTANCE.applyDayNight()
"recordLog" -> LogUtils.upLevel()
"downloadPath" -> BookHelp.upDownloadPath()
}
}

@ -5,15 +5,26 @@ import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.RssSource
import io.legado.app.help.ImageLoader
import kotlinx.android.synthetic.main.item_rss.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
class RssAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<RssSource>(context, R.layout.item_rss) {
override fun convert(holder: ItemViewHolder, item: RssSource, payloads: MutableList<Any>) {
with(holder.itemView) {
tv_name.text = item.sourceName
ImageLoader.load(context, item.sourceIcon)
.centerCrop()
.placeholder(R.drawable.image_rss)
.error(R.drawable.image_rss)
.setAsBitmap(iv_icon)
onClick { callBack.openRss(item) }
}
}
interface CallBack {
fun openRss()
fun openRss(rssSource: RssSource)
}
}

@ -1,15 +1,24 @@
package io.legado.app.ui.main.rss
import android.os.Bundle
import android.util.Log
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.LinearLayout
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.GridLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseFragment
import io.legado.app.data.entities.RssSource
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.cancelButton
import io.legado.app.lib.dialogs.yesButton
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.rss.article.RssArticlesActivity
import io.legado.app.ui.rss.source.manage.RssSourceActivity
import io.legado.app.utils.startActivity
import kotlinx.android.synthetic.main.fragment_rss.*
@ -34,6 +43,25 @@ class RssFragment : BaseFragment(R.layout.fragment_rss),
super.onCompatOptionsItemSelected(item)
when (item.itemId) {
R.id.menu_rss_config -> startActivity<RssSourceActivity>()
R.id.menu_rss_add -> {
alert {
title = "快速添加并预览"
val layout = LinearLayout(activity)
val urlEdit = EditText(activity)
urlEdit.hint = "输入RSS地址"
urlEdit.width = 800
layout.gravity = Gravity.CENTER
layout.addView(urlEdit)
customView = layout
cancelButton{
Log.i("RSS","Quick Add URL cancel")
}
yesButton{
Log.i("RSS","Quick Add URL: ${urlEdit.text}")
startActivity<RssArticlesActivity>("url" to urlEdit.text.toString().trim())
}
}.show()
}
}
}
@ -50,7 +78,7 @@ class RssFragment : BaseFragment(R.layout.fragment_rss),
})
}
override fun openRss() {
override fun openRss(rssSource: RssSource) {
startActivity<RssArticlesActivity>(Pair("url", rssSource.sourceUrl))
}
}

@ -29,7 +29,6 @@ import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.splitNotBlank
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_book_group.view.tv_group
import kotlinx.android.synthetic.main.item_group_manage.view.*
import org.jetbrains.anko.sdk27.listeners.onClick

@ -25,7 +25,7 @@ class ReplaceEditDialog : DialogFragment(),
val dialog = ReplaceEditDialog()
id?.let {
val bundle = Bundle()
bundle.putLong("data", id)
bundle.putLong("id", id)
dialog.arguments = bundle
}
return dialog

@ -14,7 +14,7 @@ class ReplaceEditViewModel(application: Application) : BaseViewModel(application
fun initData(bundle: Bundle) {
execute {
replaceRuleData.value ?: let {
val id = bundle.getLong("data")
val id = bundle.getLong("id")
if (id > 0) {
App.db.replaceRuleDao().findById(id)?.let {
replaceRuleData.postValue(it)

@ -1,3 +1,70 @@
package io.legado.app.ui.rss.article
class RssArticlesActivity
import android.os.Bundle
import androidx.core.content.ContextCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.RssArticle
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.rss.read.ReadRssActivity
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_rss_artivles.*
import org.jetbrains.anko.startActivity
class RssArticlesActivity : VMBaseActivity<RssArticlesViewModel>(R.layout.activity_rss_artivles),
RssArticlesAdapter.CallBack {
override val viewModel: RssArticlesViewModel
get() = getViewModel(RssArticlesViewModel::class.java)
private var adapter: RssArticlesAdapter? = null
private var rssArticlesData: LiveData<List<RssArticle>>? = null
override fun onActivityCreated(savedInstanceState: Bundle?) {
initView()
viewModel.titleLiveData.observe(this, Observer {
title_bar.title = it
})
intent.getStringExtra("url")?.let {
initData(it)
viewModel.loadContent(it) {
refresh_progress_bar.isAutoLoading = false
}
}
}
private fun initView() {
ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL).apply {
ContextCompat.getDrawable(baseContext, R.drawable.ic_divider)?.let {
this.setDrawable(it)
}
})
adapter = RssArticlesAdapter(this, this)
recycler_view.adapter = adapter
refresh_progress_bar.isAutoLoading = true
}
private fun initData(origin: String) {
rssArticlesData?.removeObservers(this)
rssArticlesData = App.db.rssArtivleDao().liveByOrigin(origin)
rssArticlesData?.observe(this, Observer {
adapter?.setItems(it)
})
}
override fun readRss(rssArticle: RssArticle) {
viewModel.read(rssArticle)
startActivity<ReadRssActivity>(
Pair("description", rssArticle.description),
Pair("url", rssArticle.link)
)
}
}

@ -0,0 +1,44 @@
package io.legado.app.ui.rss.article
import android.content.Context
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.RssArticle
import io.legado.app.help.ImageLoader
import io.legado.app.utils.gone
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_rss_article.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.textColorResource
class RssArticlesAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<RssArticle>(context, R.layout.item_rss_article) {
override fun convert(holder: ItemViewHolder, item: RssArticle, payloads: MutableList<Any>) {
with(holder.itemView) {
tv_title.text = item.title
tv_pub_date.text = item.pubDate
onClick {
callBack.readRss(item)
}
if (item.image.isNullOrBlank()) {
image_view.gone()
} else {
image_view.visible()
ImageLoader.load(context, item.image)
.setAsBitmap(image_view)
}
if (item.read) {
tv_title.textColorResource = R.color.tv_text_summary
} else {
tv_title.textColorResource = R.color.tv_text_default
}
}
}
interface CallBack {
fun readRss(rssArticle: RssArticle)
}
}

@ -0,0 +1,45 @@
package io.legado.app.ui.rss.article
import android.app.Application
import androidx.lifecycle.MutableLiveData
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssSource
import io.legado.app.model.Rss
import kotlinx.coroutines.Dispatchers.IO
class RssArticlesViewModel(application: Application) : BaseViewModel(application) {
val titleLiveData = MutableLiveData<String>()
fun loadContent(url: String, onFinally: () -> Unit) {
execute {
var rssSource = App.db.rssSourceDao().getByKey(url)
if (rssSource == null) {
rssSource = RssSource(sourceUrl = url)
} else {
titleLiveData.postValue(rssSource.sourceName)
}
Rss.getArticles(rssSource, this)
.onSuccess(IO) {
it?.let {
App.db.rssArtivleDao().insert(*it.toTypedArray())
}
}.onError {
toast(it.localizedMessage)
}.onFinally {
onFinally()
}
}
}
fun read(rssArticle: RssArticle) {
execute {
rssArticle.read = true
App.db.rssArtivleDao().update(rssArticle)
}
}
}

@ -3,13 +3,18 @@ package io.legado.app.ui.rss.read
import android.os.Bundle
import io.legado.app.R
import io.legado.app.base.BaseActivity
import kotlinx.android.synthetic.main.activity_rss_read.*
class ReadRssActivity : BaseActivity(R.layout.activity_read_rss) {
class ReadRssActivity : BaseActivity(R.layout.activity_rss_read) {
override fun onActivityCreated(savedInstanceState: Bundle?) {
val description = intent.getStringExtra("description")
val url = intent.getStringExtra("url")
if (description.isNullOrBlank()) {
webView.loadUrl(url)
} else {
webView.loadData("<style>img{max-width:100%}</style>$description", "text/html", "utf-8")
}
}
}

@ -1,2 +1,16 @@
package io.legado.app.ui.rss.source.debug
import android.os.Bundle
import io.legado.app.R
import io.legado.app.base.BaseActivity
class RssSourceDebugActivity : BaseActivity(R.layout.activity_source_debug) {
override fun onActivityCreated(savedInstanceState: Bundle?) {
}
}

@ -1,19 +1,212 @@
package io.legado.app.ui.rss.source.edit
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Rect
import android.os.Bundle
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.ViewTreeObserver
import android.widget.EditText
import android.widget.PopupWindow
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppConst
import io.legado.app.data.entities.EditEntity
import io.legado.app.data.entities.RssSource
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity
import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.utils.GSON
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_book_source_edit.*
import org.jetbrains.anko.displayMetrics
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.toast
import kotlin.math.abs
class RssSourceEditActivity :
VMBaseActivity<RssSourceEditViewModel>(R.layout.activity_rss_source_edit, false) {
VMBaseActivity<RssSourceEditViewModel>(R.layout.activity_rss_source_edit, false),
KeyboardToolPop.CallBack {
private var mSoftKeyboardTool: PopupWindow? = null
private var mIsSoftKeyBoardShowing = false
private val adapter = RssSourceEditAdapter()
private val sourceEntities: ArrayList<EditEntity> = ArrayList()
override val viewModel: RssSourceEditViewModel
get() = getViewModel(RssSourceEditViewModel::class.java)
override fun onActivityCreated(savedInstanceState: Bundle?) {
initView()
viewModel.sourceLiveData.observe(this, Observer {
upRecyclerView(it)
})
viewModel.initData(intent)
}
override fun onDestroy() {
super.onDestroy()
mSoftKeyboardTool?.dismiss()
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.source_edit, menu)
return super.onCompatCreateOptionsMenu(menu)
}
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_save -> {
getRssSource()?.let {
viewModel.save(it) {
setResult(Activity.RESULT_OK)
finish()
}
}
}
R.id.menu_debug_source -> {
getRssSource()?.let {
viewModel.save(it) {
startActivity<RssSourceDebugActivity>(Pair("key", it.sourceUrl))
}
}
}
R.id.menu_copy_source -> {
GSON.toJson(getRssSource())?.let { sourceStr ->
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
clipboard?.primaryClip = ClipData.newPlainText(null, sourceStr)
}
}
R.id.menu_paste_source -> viewModel.pasteSource()
}
return super.onCompatOptionsItemSelected(item)
}
private fun initView() {
ATH.applyEdgeEffectColor(recycler_view)
mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this)
window.decorView.viewTreeObserver.addOnGlobalLayoutListener(KeyboardOnGlobalChangeListener())
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.adapter = adapter
}
private fun upRecyclerView(rssSource: RssSource?) {
sourceEntities.clear()
sourceEntities.apply {
add(EditEntity("sourceName", rssSource?.sourceName, R.string.rss_source_name))
add(EditEntity("sourceUrl", rssSource?.sourceUrl, R.string.rss_source_url))
add(EditEntity("sourceIcon", rssSource?.sourceIcon, R.string.rss_source_icon))
add(EditEntity("sourceGroup", rssSource?.sourceGroup, R.string.rss_source_group))
add(EditEntity("ruleArticles", rssSource?.ruleArticles, R.string.rss_rule_articles))
add(EditEntity("ruleTitle", rssSource?.ruleTitle, R.string.rss_rule_title))
add(EditEntity("ruleAuthor", rssSource?.ruleAuthor, R.string.rss_rule_author))
add(EditEntity("ruleGuid", rssSource?.ruleGuid, R.string.rss_rule_guid))
add(EditEntity("rulePubDate", rssSource?.rulePubDate, R.string.rss_rule_date))
add(
EditEntity(
"ruleCategories",
rssSource?.ruleCategories,
R.string.rss_rule_categories
)
)
add(
EditEntity(
"ruleDescription",
rssSource?.ruleDescription,
R.string.rss_rule_description
)
)
add(EditEntity("ruleImage", rssSource?.ruleImage, R.string.rss_rule_image))
add(EditEntity("ruleLink", rssSource?.ruleLink, R.string.rss_rule_link))
add(EditEntity("ruleContent", rssSource?.ruleContent, R.string.rss_rule_content))
}
adapter.editEntities = sourceEntities
}
private fun getRssSource(): RssSource? {
val source = viewModel.sourceLiveData.value ?: RssSource()
sourceEntities.forEach {
when (it.key) {
"sourceName" -> source.sourceName = it.value ?: ""
"sourceUrl" -> source.sourceUrl = it.value ?: ""
"sourceIcon" -> source.sourceIcon = it.value ?: ""
"sourceGroup" -> source.sourceGroup = it.value
"ruleArticles" -> source.ruleArticles = it.value
"ruleTitle" -> source.ruleTitle = it.value
"ruleAuthor" -> source.ruleAuthor = it.value
"ruleGuid" -> source.ruleGuid = it.value
"rulePubDate" -> source.rulePubDate = it.value
"ruleCategories" -> source.ruleCategories = it.value
"ruleDescription" -> source.ruleDescription = it.value
"ruleImage" -> source.ruleImage = it.value
"ruleLink" -> source.ruleLink = it.value
"ruleContent" -> source.ruleContent = it.value
}
}
if (source.sourceName.isBlank() || source.sourceName.isBlank()) {
toast("名称或url不能为空")
return null
}
return source
}
override fun sendText(text: String) {
if (text.isBlank()) return
val view = window.decorView.findFocus()
if (view is EditText) {
val start = view.selectionStart
val end = view.selectionEnd
val edit = view.editableText//获取EditText的文字
if (start < 0 || start >= edit.length) {
edit.append(text)
} else {
edit.replace(start, end, text)//光标所在位置插入文字
}
}
}
private fun showKeyboardTopPopupWindow() {
mSoftKeyboardTool?.isShowing?.let { if (it) return }
if (!isFinishing) {
mSoftKeyboardTool?.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0)
}
}
private fun closePopupWindow() {
mSoftKeyboardTool?.let {
if (it.isShowing) {
it.dismiss()
}
}
}
private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
val rect = Rect()
// 获取当前页面窗口的显示范围
window.decorView.getWindowVisibleDisplayFrame(rect)
val screenHeight = this@RssSourceEditActivity.displayMetrics.heightPixels
val keyboardHeight = screenHeight - rect.bottom // 输入法的高度
val preShowing = mIsSoftKeyBoardShowing
if (abs(keyboardHeight) > screenHeight / 5) {
mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法
recycler_view.setPadding(0, 0, 0, 100)
showKeyboardTopPopupWindow()
} else {
mIsSoftKeyBoardShowing = false
recycler_view.setPadding(0, 0, 0, 0)
if (preShowing) {
closePopupWindow()
}
}
}
}
}

@ -0,0 +1,85 @@
package io.legado.app.ui.rss.source.edit
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.data.entities.EditEntity
import kotlinx.android.synthetic.main.item_source_edit.view.*
class RssSourceEditAdapter : RecyclerView.Adapter<RssSourceEditAdapter.MyViewHolder>() {
var editEntities: ArrayList<EditEntity> = ArrayList()
set(value) {
field = value
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
LayoutInflater.from(
parent.context
).inflate(R.layout.item_source_edit, parent, false)
)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(editEntities[position])
}
override fun getItemCount(): Int {
return editEntities.size
}
class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(editEntity: EditEntity) = with(itemView) {
if (editText.getTag(R.id.tag1) == null) {
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
editText.isCursorVisible = false
editText.isCursorVisible = true
editText.isFocusable = true
editText.isFocusableInTouchMode = true
}
override fun onViewDetachedFromWindow(v: View) {
}
}
editText.addOnAttachStateChangeListener(listener)
editText.setTag(R.id.tag1, listener)
}
editText.getTag(R.id.tag2)?.let {
if (it is TextWatcher) {
editText.removeTextChangedListener(it)
}
}
editText.setText(editEntity.value)
textInputLayout.hint = context.getString(editEntity.hint)
val textWatcher = object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: Editable?) {
editEntity.value = (s?.toString())
}
}
editText.addTextChangedListener(textWatcher)
editText.setTag(R.id.tag2, textWatcher)
}
}
}

@ -1,9 +1,66 @@
package io.legado.app.ui.rss.source.edit
import android.app.Application
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import androidx.lifecycle.MutableLiveData
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.RssSource
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
class RssSourceEditViewModel(application: Application) : BaseViewModel(application) {
val sourceLiveData: MutableLiveData<RssSource> = MutableLiveData()
var oldSourceUrl: String? = null
fun initData(intent: Intent) {
execute {
val key = intent.getStringExtra("data")
var source: RssSource? = null
if (key != null) {
source = App.db.rssSourceDao().getByKey(key)
}
source?.let {
oldSourceUrl = it.sourceUrl
sourceLiveData.postValue(it)
} ?: let {
sourceLiveData.postValue(RssSource().apply {
customOrder = App.db.rssSourceDao().maxOrder + 1
})
}
}
}
fun save(rssSource: RssSource, success: (() -> Unit)) {
execute {
oldSourceUrl?.let {
if (oldSourceUrl != rssSource.sourceUrl) {
App.db.rssSourceDao().delete(it)
}
}
oldSourceUrl = rssSource.sourceUrl
App.db.rssSourceDao().insert(rssSource)
}.onSuccess {
success()
}.onError {
toast(it.localizedMessage)
}
}
fun pasteSource() {
execute {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
clipboard?.primaryClip?.let {
if (it.itemCount > 0) {
val json = it.getItemAt(0).text.toString()
GSON.fromJsonObject<RssSource>(json)?.let { source ->
sourceLiveData.postValue(source)
} ?: toast("格式不对")
}
}
}
}
}

@ -0,0 +1,41 @@
package io.legado.app.ui.rss.source.manage
import androidx.recyclerview.widget.DiffUtil
import io.legado.app.data.entities.RssSource
class DiffCallBack(
private val oldItems: List<RssSource>,
private val newItems: List<RssSource>
) : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return oldItem.sourceUrl == newItem.sourceUrl
}
override fun getOldListSize(): Int {
return oldItems.size
}
override fun getNewListSize(): Int {
return newItems.size
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return oldItem.sourceName == newItem.sourceName
&& oldItem.enabled == newItem.enabled
}
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return when {
oldItem.sourceName == newItem.sourceName
&& oldItem.enabled != newItem.enabled -> 2
else -> null
}
}
}

@ -3,11 +3,16 @@ package io.legado.app.ui.rss.source.manage
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.SubMenu
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.RssSource
@ -16,6 +21,7 @@ import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.utils.getViewModel
import io.legado.app.utils.splitNotBlank
import kotlinx.android.synthetic.main.activity_rss_source.*
import kotlinx.android.synthetic.main.view_search.*
import kotlinx.android.synthetic.main.view_title_bar.*
@ -29,11 +35,16 @@ class RssSourceActivity : VMBaseActivity<RssSourceViewModel>(R.layout.activity_r
get() = getViewModel(RssSourceViewModel::class.java)
private lateinit var adapter: RssSourceAdapter
private var sourceLiveData: LiveData<List<RssSource>>? = null
private var groups = hashSetOf<String>()
private var groupMenu: SubMenu? = null
override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar)
initRecyclerView()
initSearchView()
initLiveDataGroup()
initLiveDataSource()
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
@ -41,9 +52,20 @@ class RssSourceActivity : VMBaseActivity<RssSourceViewModel>(R.layout.activity_r
return super.onCompatCreateOptionsMenu(menu)
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
groupMenu = menu?.findItem(R.id.menu_group)?.subMenu
upGroupMenu()
return super.onPrepareOptionsMenu(menu)
}
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_add_book_source -> startActivity<RssSourceEditActivity>()
R.id.menu_add -> startActivity<RssSourceEditActivity>()
R.id.menu_select_all -> adapter.selectAll()
R.id.menu_revert_selection -> adapter.revertSelection()
R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelectionIds())
R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelectionIds())
R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelectionIds())
}
return super.onCompatOptionsItemSelected(item)
}
@ -81,24 +103,57 @@ class RssSourceActivity : VMBaseActivity<RssSourceViewModel>(R.layout.activity_r
})
}
override fun del(source: RssSource) {
private fun initLiveDataGroup() {
App.db.rssSourceDao().liveGroup().observe(this, Observer {
groups.clear()
it.map { group ->
groups.addAll(group.splitNotBlank(",", ";"))
}
upGroupMenu()
})
}
private fun upGroupMenu() {
groupMenu?.removeGroup(R.id.source_group)
groups.map {
groupMenu?.add(R.id.source_group, Menu.NONE, Menu.NONE, it)
}
}
override fun edit(source: RssSource) {
private fun initLiveDataSource(key: String? = null) {
sourceLiveData?.removeObservers(this)
sourceLiveData =
if (key.isNullOrBlank()) {
App.db.rssSourceDao().liveAll()
} else {
App.db.rssSourceDao().liveSearch("%$key%")
}
sourceLiveData?.observe(this, Observer {
val diffResult = DiffUtil
.calculateDiff(DiffCallBack(adapter.getItems(), it))
adapter.setItemsNoNotify(it)
diffResult.dispatchUpdatesTo(adapter)
})
}
override fun del(source: RssSource) {
viewModel.del(source)
}
override fun update(vararg source: RssSource) {
override fun edit(source: RssSource) {
startActivity<RssSourceEditActivity>(Pair("data", source.sourceUrl))
}
override fun update(vararg source: RssSource) {
viewModel.update(*source)
}
override fun toTop(source: RssSource) {
viewModel.topSource(source)
}
override fun upOrder() {
viewModel.upOrder()
}
}

@ -1,18 +1,95 @@
package io.legado.app.ui.rss.source.manage
import android.content.Context
import android.view.Menu
import android.widget.PopupMenu
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.RssSource
import io.legado.app.help.ItemTouchCallback
import io.legado.app.lib.theme.backgroundColor
import kotlinx.android.synthetic.main.item_rss_source.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
class RssSourceAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<RssSource>(context, R.layout.item_rss_source),
ItemTouchCallback.OnItemTouchCallbackListener {
override fun convert(holder: ItemViewHolder, item: RssSource, payloads: MutableList<Any>) {
private val selectedIds = linkedSetOf<String>()
fun selectAll() {
getItems().forEach {
selectedIds.add(it.sourceUrl)
}
notifyItemRangeChanged(0, itemCount, 1)
}
fun revertSelection() {
getItems().forEach {
if (selectedIds.contains(it.sourceUrl)) {
selectedIds.remove(it.sourceUrl)
} else {
selectedIds.add(it.sourceUrl)
}
}
notifyItemRangeChanged(0, itemCount, 1)
}
fun getSelectionIds(): LinkedHashSet<String> {
val selection = linkedSetOf<String>()
getItems().map {
if (selectedIds.contains(it.sourceUrl)) {
selection.add(it.sourceUrl)
}
}
return selection
}
override fun convert(holder: ItemViewHolder, item: RssSource, payloads: MutableList<Any>) {
with(holder.itemView) {
if (payloads.isEmpty()) {
this.setBackgroundColor(context.backgroundColor)
if (item.sourceGroup.isNullOrEmpty()) {
cb_source.text = item.sourceName
} else {
cb_source.text =
String.format("%s (%s)", item.sourceName, item.sourceGroup)
}
swt_enabled.isChecked = item.enabled
swt_enabled.onClick {
item.enabled = swt_enabled.isChecked
callBack.update(item)
}
cb_source.isChecked = selectedIds.contains(item.sourceUrl)
cb_source.setOnClickListener {
if (cb_source.isChecked) {
selectedIds.add(item.sourceUrl)
} else {
selectedIds.remove(item.sourceUrl)
}
}
iv_edit.onClick { callBack.edit(item) }
iv_menu_more.onClick {
val popupMenu = PopupMenu(context, it)
popupMenu.menu.add(Menu.NONE, R.id.menu_top, Menu.NONE, R.string.to_top)
popupMenu.menu.add(Menu.NONE, R.id.menu_del, Menu.NONE, R.string.delete)
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.menu_top -> callBack.toTop(item)
R.id.menu_del -> callBack.del(item)
}
true
}
popupMenu.show()
}
} else {
when (payloads[0]) {
1 -> cb_source.isChecked = selectedIds.contains(item.sourceUrl)
2 -> swt_enabled.isChecked = item.enabled
}
}
}
}

@ -1,8 +1,52 @@
package io.legado.app.ui.rss.source.manage
import android.app.Application
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.RssSource
class RssSourceViewModel(application: Application) : BaseViewModel(application) {
fun topSource(rssSource: RssSource) {
execute {
rssSource.customOrder = App.db.rssSourceDao().minOrder - 1
App.db.rssSourceDao().insert(rssSource)
}
}
fun del(rssSource: RssSource) {
execute { App.db.rssSourceDao().delete(rssSource) }
}
fun update(vararg rssSource: RssSource) {
execute { App.db.rssSourceDao().update(*rssSource) }
}
fun upOrder() {
execute {
val sources = App.db.rssSourceDao().all
for ((index: Int, source: RssSource) in sources.withIndex()) {
source.customOrder = index + 1
}
App.db.rssSourceDao().update(*sources.toTypedArray())
}
}
fun enableSelection(ids: LinkedHashSet<String>) {
execute {
App.db.rssSourceDao().enableSection(*ids.toTypedArray())
}
}
fun disableSelection(ids: LinkedHashSet<String>) {
execute {
App.db.rssSourceDao().disableSection(*ids.toTypedArray())
}
}
fun delSelection(ids: LinkedHashSet<String>) {
execute {
App.db.rssSourceDao().delSection(*ids.toTypedArray())
}
}
}

@ -18,7 +18,7 @@ import org.jetbrains.anko.sdk27.listeners.onClick
class KeyboardToolPop(
context: Context,
private val chars: List<String>,
val onClickListener: OnClickListener?
val callBack: CallBack?
) : PopupWindow(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {
init {
@ -45,13 +45,13 @@ class KeyboardToolPop(
override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList<Any>) {
with(holder.itemView) {
text_view.text = item
onClick { onClickListener?.click(item) }
onClick { callBack?.sendText(item) }
}
}
}
interface OnClickListener {
fun click(text: String)
interface CallBack {
fun sendText(text: String)
}
}

@ -16,15 +16,15 @@ class RefreshProgressBar @JvmOverloads constructor(
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
internal var a = 1
var maxProgress = 100
private var a = 1
private var durProgress = 0
var secondMaxProgress = 100
private var secondDurProgress = 0
var maxProgress = 100
var secondMaxProgress = 100
var bgColor = 0x00000000
var secondColor = -0x3e3e3f
var fontColor = -0xc9c9ca
var speed = 1
var speed = 2
var secondFinalProgress = 0
private set
private var paint: Paint = Paint()

@ -22,6 +22,7 @@ import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.view.View
import android.widget.ImageView
import kotlin.math.roundToInt
object Utils {
@ -29,7 +30,7 @@ object Utils {
private val sCanvas = Canvas()
fun dp2Px(dp: Int): Int {
return Math.round(dp * DENSITY)
return (dp * DENSITY).roundToInt()
}
fun createBitmapFromView(view: View): Bitmap? {
@ -55,7 +56,7 @@ object Utils {
return bitmap
}
fun createBitmapSafely(
private fun createBitmapSafely(
width: Int,
height: Int,
config: Bitmap.Config,

@ -7,6 +7,8 @@ import android.graphics.Path
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import io.legado.app.R
import io.legado.app.utils.dp
import kotlin.math.max
class FilletImageView : AppCompatImageView {
internal var width: Float = 0.toFloat()
@ -29,7 +31,7 @@ class FilletImageView : AppCompatImageView {
private fun init(context: Context, attrs: AttributeSet) {
// 读取配置
val array = context.obtainStyledAttributes(attrs, R.styleable.FilletImageView)
val defaultRadius = 5
val defaultRadius = 5.dp
val radius = array.getDimensionPixelOffset(R.styleable.FilletImageView_radius, defaultRadius)
leftTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_left_top_radius, defaultRadius)
rightTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_right_top_radius, defaultRadius)
@ -62,11 +64,11 @@ class FilletImageView : AppCompatImageView {
override fun onDraw(canvas: Canvas) {
//这里做下判断,只有图片的宽高大于设置的圆角距离的时候才进行裁剪
val maxLeft = Math.max(leftTopRadius, leftBottomRadius)
val maxRight = Math.max(rightTopRadius, rightBottomRadius)
val maxLeft = max(leftTopRadius, leftBottomRadius)
val maxRight = max(rightTopRadius, rightBottomRadius)
val minWidth = maxLeft + maxRight
val maxTop = Math.max(leftTopRadius, rightTopRadius)
val maxBottom = Math.max(leftBottomRadius, rightBottomRadius)
val maxTop = max(leftTopRadius, rightTopRadius)
val maxBottom = max(leftBottomRadius, rightBottomRadius)
val minHeight = maxTop + maxBottom
if (width >= minWidth && height > minHeight) {
@SuppressLint("DrawAllocation") val path = Path()

@ -10,7 +10,6 @@ import android.widget.ImageView
import androidx.appcompat.widget.AppCompatImageView
import io.legado.app.R
import io.legado.app.constant.AppConst.TIME_FORMAT
import io.legado.app.help.ImageLoader
import io.legado.app.help.ReadBookConfig
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.view_book_page.view.*
@ -90,10 +89,7 @@ class ContentView : FrameLayout {
}
fun setBg(bg: Drawable?) {
//all supported
ImageLoader.load(context, bg)
.centerCrop()
.setAsDrawable(bgImage)
bgImage.background = bg
}
fun upTime() {

@ -23,7 +23,49 @@ import kotlin.math.min
* 本地缓存
*/
class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) {
private lateinit var mCache: ACacheManager
companion object {
const val TIME_HOUR = 60 * 60
const val TIME_DAY = TIME_HOUR * 24
private const val MAX_SIZE = 1000 * 1000 * 50 // 50 mb
private const val MAX_COUNT = Integer.MAX_VALUE // 不限制存放数据的数量
private val mInstanceMap = HashMap<String, ACache>()
@JvmOverloads
fun get(
ctx: Context,
cacheName: String = "ACache",
maxSize: Long = MAX_SIZE.toLong(),
maxCount: Int = MAX_COUNT,
cacheDir: Boolean = true
): ACache {
val f = if (cacheDir) File(ctx.cacheDir, cacheName) else File(ctx.filesDir, cacheName)
return get(f, maxSize, maxCount)
}
@JvmOverloads
fun get(
cacheDir: File,
maxSize: Long = MAX_SIZE.toLong(),
maxCount: Int = MAX_COUNT
): ACache {
synchronized(this) {
var manager = mInstanceMap[cacheDir.absoluteFile.toString() + myPid()]
if (manager == null) {
manager = ACache(cacheDir, maxSize, maxCount)
mInstanceMap[cacheDir.absolutePath + myPid()] = manager
}
return manager
}
}
private fun myPid(): String {
return "_" + android.os.Process.myPid()
}
}
private var mCache: ACacheManager? = null
init {
try {
@ -48,6 +90,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* @param value 保存的String数据
*/
fun put(key: String, value: String) {
mCache?.let { mCache ->
try {
val file = mCache.newFile(key)
file.writeText(value)
@ -56,6 +99,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
e.printStackTrace()
}
}
}
/**
* 保存 String数据 缓存中
@ -74,25 +118,27 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* @return String 数据
*/
fun getAsString(key: String): String? {
mCache?.let { mCache ->
val file = mCache[key]
if (!file.exists())
return null
var removeFile = false
return try {
try {
val text = file.readText()
if (!Utils.isDue(text)) {
Utils.clearDateInfo(text)
return Utils.clearDateInfo(text)
} else {
removeFile = true
null
}
} catch (e: IOException) {
null
e.printStackTrace()
} finally {
if (removeFile)
remove(key)
}
}
return null
}
// =======================================
// ========== JSONObject 数据 读写 =========
@ -184,10 +230,12 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* @param value 保存的数据
*/
fun put(key: String, value: ByteArray) {
mCache?.let { mCache ->
val file = mCache.newFile(key)
file.writeBytes(value)
mCache.put(file)
}
}
/**
* 保存 byte数据 缓存中
@ -206,6 +254,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* @return byte 数据
*/
fun getAsBinary(key: String): ByteArray? {
mCache?.let { mCache ->
var removeFile = false
try {
val file = mCache[key]
@ -221,12 +270,13 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
}
} catch (e: Exception) {
e.printStackTrace()
return null
} finally {
if (removeFile)
remove(key)
}
}
return null
}
/**
* 保存 Serializable数据到 缓存中
@ -269,7 +319,6 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
return ois.readObject()
} catch (e: Exception) {
e.printStackTrace()
return null
} finally {
try {
bais?.close()
@ -371,6 +420,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* @return value 缓存的文件
*/
fun file(key: String): File? {
mCache?.let { mCache ->
try {
val f = mCache.newFile(key)
if (f.exists()) {
@ -379,7 +429,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
} catch (e: Exception) {
e.printStackTrace()
}
}
return null
}
@ -389,14 +439,14 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* @return 是否移除成功
*/
fun remove(key: String): Boolean {
return mCache.remove(key)
return mCache?.remove(key) == true
}
/**
* 清除所有数据
*/
fun clear() {
mCache.clear()
mCache?.clear()
}
/**
@ -726,44 +776,4 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
}
}
companion object {
const val TIME_HOUR = 60 * 60
const val TIME_DAY = TIME_HOUR * 24
private const val MAX_SIZE = 1000 * 1000 * 50 // 50 mb
private const val MAX_COUNT = Integer.MAX_VALUE // 不限制存放数据的数量
private val mInstanceMap = HashMap<String, ACache>()
@JvmOverloads
fun get(
ctx: Context,
cacheName: String = "ACache",
maxSize: Long = MAX_SIZE.toLong(),
maxCount: Int = MAX_COUNT,
cacheDir: Boolean = true
): ACache {
val f = if (cacheDir) File(ctx.cacheDir, cacheName) else File(ctx.filesDir, cacheName)
return get(f, maxSize, maxCount)
}
@JvmOverloads
fun get(
cacheDir: File,
maxSize: Long = MAX_SIZE.toLong(),
maxCount: Int = MAX_COUNT
): ACache {
synchronized(this) {
var manager = mInstanceMap[cacheDir.absoluteFile.toString() + myPid()]
if (manager == null) {
manager = ACache(cacheDir, maxSize, maxCount)
mInstanceMap[cacheDir.absolutePath + myPid()] = manager
}
return manager
}
}
private fun myPid(): String {
return "_" + android.os.Process.myPid()
}
}
}

@ -0,0 +1,255 @@
package io.legado.app.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Bitmap.Config
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur
import android.view.View
import io.legado.app.App
import java.io.IOException
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.min
import kotlin.math.sqrt
@Suppress("unused", "WeakerAccess")
object BitmapUtil {
/**
* 从path中获取图片信息,在通过BitmapFactory.decodeFile(String path)方法将突破转成Bitmap时
* 遇到大一些的图片我们经常会遇到OOM(Out Of Memory)的问题所以用到了我们上面提到的BitmapFactory.Options这个类
*
* @param path 文件路径
* @param width 想要显示的图片的宽度
* @param height 想要显示的图片的高度
* @return
*/
fun decodeBitmap(path: String, width: Int, height: Int): Bitmap {
val op = BitmapFactory.Options()
// inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight;
op.inJustDecodeBounds = true
BitmapFactory.decodeFile(path, op) //获取尺寸信息
//获取比例大小
val wRatio = ceil((op.outWidth / width).toDouble()).toInt()
val hRatio = ceil((op.outHeight / height).toDouble()).toInt()
//如果超出指定大小,则缩小相应的比例
if (wRatio > 1 && hRatio > 1) {
if (wRatio > hRatio) {
op.inSampleSize = wRatio
} else {
op.inSampleSize = hRatio
}
}
op.inJustDecodeBounds = false
return BitmapFactory.decodeFile(path, op)
}
/** 从path中获取Bitmap图片
* @param path 图片路径
* @return
*/
fun decodeBitmap(path: String): Bitmap {
val opts = BitmapFactory.Options()
opts.inJustDecodeBounds = true
BitmapFactory.decodeFile(path, opts)
opts.inSampleSize = computeSampleSize(opts, -1, 128 * 128)
opts.inJustDecodeBounds = false
return BitmapFactory.decodeFile(path, opts)
}
/**
* 以最省内存的方式读取本地资源的图片
* @param context 设备上下文
* @param resId 资源ID
* @return
*/
fun decodeBitmap(context: Context, resId: Int): Bitmap? {
val opt = BitmapFactory.Options()
opt.inPreferredConfig = Config.RGB_565
//获取资源图片
val `is` = context.resources.openRawResource(resId)
return BitmapFactory.decodeStream(`is`, null, opt)
}
/**
* @param context 设备上下文
* @param resId 资源ID
* @param width
* @param height
* @return
*/
fun decodeBitmap(context: Context, resId: Int, width: Int, height: Int): Bitmap? {
var inputStream = context.resources.openRawResource(resId)
val op = BitmapFactory.Options()
// inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight;
op.inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream, null, op) //获取尺寸信息
//获取比例大小
val wRatio = ceil((op.outWidth / width).toDouble()).toInt()
val hRatio = ceil((op.outHeight / height).toDouble()).toInt()
//如果超出指定大小,则缩小相应的比例
if (wRatio > 1 && hRatio > 1) {
if (wRatio > hRatio) {
op.inSampleSize = wRatio
} else {
op.inSampleSize = hRatio
}
}
inputStream = context.resources.openRawResource(resId)
op.inJustDecodeBounds = false
return BitmapFactory.decodeStream(inputStream, null, op)
}
/**
* @param context 设备上下文
* @param fileNameInAssets Assets里面文件的名称
* @param width 图片的宽度
* @param height 图片的高度
* @return Bitmap
* @throws IOException
*/
@Throws(IOException::class)
fun decodeBitmap(context: Context, fileNameInAssets: String, width: Int, height: Int): Bitmap? {
var inputStream = context.assets.open(fileNameInAssets)
val op = BitmapFactory.Options()
// inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight;
op.inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream, null, op) //获取尺寸信息
//获取比例大小
val wRatio = ceil((op.outWidth / width).toDouble()).toInt()
val hRatio = ceil((op.outHeight / height).toDouble()).toInt()
//如果超出指定大小,则缩小相应的比例
if (wRatio > 1 && hRatio > 1) {
if (wRatio > hRatio) {
op.inSampleSize = wRatio
} else {
op.inSampleSize = hRatio
}
}
inputStream = context.assets.open(fileNameInAssets)
op.inJustDecodeBounds = false
return BitmapFactory.decodeStream(inputStream, null, op)
}
//图片不被压缩
fun convertViewToBitmap(view: View, bitmapWidth: Int, bitmapHeight: Int): Bitmap {
val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888)
view.draw(Canvas(bitmap))
return bitmap
}
/**
* @param options
* @param minSideLength
* @param maxNumOfPixels
* @return
* 设置恰当的inSampleSize是解决该问题的关键之一BitmapFactory.Options提供了另一个成员inJustDecodeBounds
* 设置inJustDecodeBounds为true后decodeFile并不分配空间但可计算出原始图片的长度和宽度即opts.width和opts.height
* 有了这两个参数再通过一定的算法即可得到一个恰当的inSampleSize
* 查看Android源码Android提供了下面这种动态计算的方法
*/
fun computeSampleSize(
options: BitmapFactory.Options,
minSideLength: Int,
maxNumOfPixels: Int
): Int {
val initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels)
var roundedSize: Int
if (initialSize <= 8) {
roundedSize = 1
while (roundedSize < initialSize) {
roundedSize = roundedSize shl 1
}
} else {
roundedSize = (initialSize + 7) / 8 * 8
}
return roundedSize
}
private fun computeInitialSampleSize(
options: BitmapFactory.Options,
minSideLength: Int,
maxNumOfPixels: Int
): Int {
val w = options.outWidth.toDouble()
val h = options.outHeight.toDouble()
val lowerBound = if (maxNumOfPixels == -1)
1
else
ceil(sqrt(w * h / maxNumOfPixels)).toInt()
val upperBound = if (minSideLength == -1) 128 else min(
floor(w / minSideLength),
floor(h / minSideLength)
).toInt()
if (upperBound < lowerBound) {
// return the larger one when there is no overlapping zone.
return lowerBound
}
return if (maxNumOfPixels == -1 && minSideLength == -1) {
1
} else if (minSideLength == -1) {
lowerBound
} else {
upperBound
}
}
/**
* 高斯模糊
*/
fun stackBlur(srcBitmap: Bitmap?): Bitmap? {
if (srcBitmap == null) return null
val rs = RenderScript.create(App.INSTANCE)
val blurredBitmap = srcBitmap.copy(Config.ARGB_8888, true)
//分配用于渲染脚本的内存
val input = Allocation.createFromBitmap(
rs,
blurredBitmap,
Allocation.MipmapControl.MIPMAP_FULL,
Allocation.USAGE_SHARED
)
val output = Allocation.createTyped(rs, input.type)
//加载我们想要使用的特定脚本的实例。
val script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
script.setInput(input)
//设置模糊半径
script.setRadius(8f)
//启动 ScriptIntrinsicBlur
script.forEach(output)
//将输出复制到模糊的位图
output.copyTo(blurredBitmap)
return blurredBitmap
}
}

@ -11,66 +11,52 @@ import java.util.logging.Formatter
object LogUtils {
const val TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"
private val logger: Logger by lazy {
Logger.getGlobal().apply {
addFileHandler(
this,
Level.INFO,
FileHelp.getCachePath()
)
}
@JvmStatic
fun d(tag: String, msg: String) {
logger.log(Level.INFO, "$tag $msg")
}
fun log(msg: String) {
if (App.INSTANCE.getPrefBoolean("recordLog")) {
logger.log(Level.INFO, msg)
}
@JvmStatic
fun e(tag: String, msg: String) {
logger.log(Level.WARNING, "$tag $msg")
}
private const val DATE_PATTERN = "yyyy-MM-dd"
const val TIME_PATTERN = "HH:mm:ss"
/**
* 为log添加控制台handler
*
* @param log 要添加handler的log
* @param level 控制台的输出等级
*/
fun addConsoleHandler(log: Logger, level: Level) {
// 控制台输出的handler
val consoleHandler = ConsoleHandler()
// 设置控制台输出的等级(如果ConsoleHandler的等级高于或者等于log的level,则按照FileHandler的level输出到控制台,如果低于,则按照Log等级输出)
consoleHandler.level = level
// 添加控制台的handler
log.addHandler(consoleHandler)
private val logger: Logger by lazy {
Logger.getGlobal().apply {
addHandler(fileHandler)
}
}
/**
* 为log添加文件输出Handler
*
* @param log 要添加文件输出handler的log
* @param level log输出等级
* @param filePath 指定文件全路径
*/
fun addFileHandler(log: Logger, level: Level, filePath: String) {
var fileHandler: FileHandler? = null
try {
fileHandler =
FileHandler(filePath + File.separator + getCurrentDateStr(DATE_PATTERN) + ".log")
// 设置输出文件的等级(如果FileHandler的等级高于或者等于log的level,则按照FileHandler的level输出到文件,如果低于,则按照Log等级输出)
fileHandler.level = level
fileHandler.formatter = object : Formatter() {
private val fileHandler by lazy {
val logFolder = FileHelp.getCachePath() + File.separator + "logs"
FileHelp.getFolder(logFolder)
FileHandler(
logFolder + File.separator + "app.log",
2048,
10
).apply {
formatter = object : Formatter() {
override fun format(record: LogRecord): String {
// 设置文件输出格式
return (getCurrentDateStr(TIME_PATTERN) + ": " + record.message + "\n")
}
}
} catch (e: Exception) {
e.printStackTrace()
level = if (App.INSTANCE.getPrefBoolean("recordLog")) {
Level.INFO
} else {
Level.OFF
}
}
}
fun upLevel() {
fileHandler.level = if (App.INSTANCE.getPrefBoolean("recordLog")) {
Level.INFO
} else {
Level.OFF
}
// 添加输出文件handler
log.addHandler(fileHandler)
}
/**

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

@ -28,7 +28,7 @@
android:layout_width="86dp"
android:layout_height="120dp"
android:layout_margin="10dp"
android:src="@drawable/img_cover_default"
android:src="@drawable/image_cover_default"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title_bar" />
@ -165,7 +165,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="简介"
android:text="@string/book_intro"
android:textColor="@color/tv_text_default"
android:textSize="16sp" />
@ -192,23 +192,16 @@
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toRightOf="@+id/fg">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp"
android:text="目录"
android:text="@string/chapter_list"
android:textColor="@color/tv_text_default"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_chapter_base_info"
android:layout_width="match_parent"
@ -223,7 +216,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="当前位置:" />
android:text="@string/dur_pos" />
<TextView
android:id="@+id/tv_current_chapter_info"
@ -245,11 +238,21 @@
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/go_to_top"
android:padding="10dp"
android:src="@drawable/ic_exchange_order"
android:src="@drawable/ic_arrow_drop_up"
android:tooltipText="@string/go_to_top"
app:tint="@color/tv_text_default"
tools:ignore="UnusedAttribute" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_chapter_bottom"
android:layout_width="36dp"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/go_to_bottom"
android:src="@drawable/ic_arrow_drop_down"
android:tooltipText="@string/go_to_bottom"
app:tint="@color/tv_text_default"
tools:ignore="UnusedAttribute" />
</LinearLayout>
</LinearLayout>

@ -25,7 +25,7 @@
android:layout_height="260dp"
android:layout_gravity="center"
android:contentDescription="@string/img_cover"
android:src="@drawable/img_cover_default"
android:src="@drawable/image_cover_default"
app:layout_constraintTop_toBottomOf="@+id/title_bar"
app:layout_constraintBottom_toTopOf="@+id/ll_player_progress"
app:layout_constraintLeft_toLeftOf="parent"

@ -19,7 +19,7 @@
android:layout_height="120dp"
android:layout_margin="10dp"
android:scaleType="centerCrop"
android:src="@drawable/img_cover_default"
android:src="@drawable/image_cover_default"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title_bar" />
@ -154,7 +154,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="简介"
android:text="@string/book_intro"
android:textColor="@color/tv_text_default"
android:textSize="16sp" />
@ -184,23 +184,16 @@
android:layout_height="5dp"
android:background="@color/bg_divider_line" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp"
android:text="目录"
android:text="@string/chapter_list"
android:textColor="@color/tv_text_default"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_chapter_base_info"
android:layout_width="match_parent"
@ -215,7 +208,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="当前位置:" />
android:text="@string/dur_pos" />
<TextView
android:id="@+id/tv_current_chapter_info"
@ -237,11 +230,21 @@
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/go_to_top"
android:padding="10dp"
android:src="@drawable/ic_exchange_order"
android:src="@drawable/ic_arrow_drop_up"
android:tooltipText="@string/go_to_top"
app:tint="@color/tv_text_default"
tools:ignore="UnusedAttribute" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_chapter_bottom"
android:layout_width="36dp"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/go_to_bottom"
android:src="@drawable/ic_arrow_drop_down"
android:tooltipText="@string/go_to_bottom"
app:tint="@color/tv_text_default"
tools:ignore="UnusedAttribute" />
</LinearLayout>
</LinearLayout>
@ -284,8 +287,6 @@
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_btn_accent_bg"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:includeFontPadding="false"
android:text="@string/reading"

@ -34,7 +34,7 @@
android:layout_height="126dp"
android:contentDescription="@string/img_cover"
android:scaleType="centerCrop"
android:src="@drawable/img_cover_default" />
android:src="@drawable/image_cover_default" />
<LinearLayout
android:layout_width="match_parent"

@ -36,6 +36,61 @@
</io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout>
<LinearLayout
android:id="@+id/ll_history"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/background"
android:orientation="vertical"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
app:layout_constraintTop_toBottomOf="@id/title_bar"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/tv_book_show"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/bookshelf"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_bookshelf_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_history"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="6dp"
android:text="@string/searchHistory" />
<TextView
android:id="@+id/tv_clear_history"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/clear" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_history_key"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fb_stop"
android:layout_width="wrap_content"

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>

@ -0,0 +1,34 @@
<?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="match_parent"
android:orientation="vertical">
<io.legado.app.ui.widget.TitleBar
android:id="@+id/title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<io.legado.app.ui.widget.anima.RefreshProgressBar
android:id="@+id/refresh_progress_bar"
android:layout_width="match_parent"
android:layout_height="2dp"
app:layout_constraintTop_toBottomOf="@id/title_bar" />
<io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout
android:id="@+id/content_view"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/refresh_progress_bar">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<io.legado.app.ui.widget.TitleBar
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@ -11,7 +12,12 @@
android:layout_height="wrap_content"
app:contentInsetStartWithNavigation="0dp"
app:displayHomeAsUp="true"
app:fitStatusBar="false"
app:title="@string/rss_source_edit" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save