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. 42
      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. 37
      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. 60
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  58. 31
      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. 196
      app/src/main/java/io/legado/app/utils/ACache.kt
  86. 255
      app/src/main/java/io/legado/app/utils/BitmapUtil.kt
  87. 82
      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. 41
      app/src/main/res/layout-land/activity_book_info.xml
  92. 2
      app/src/main/res/layout/activity_audio_play.xml
  93. 43
      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:name=".help.permission.PermissionActivity"
android:theme="@style/Activity.Permission" /> android:theme="@style/Activity.Permission" />
<activity <activity
android:name=".ui.book.source.edit.SourceEditActivity" android:name=".ui.book.source.edit.BookSourceEditActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout" android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask" android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" /> android:windowSoftInputMode="adjustResize|stateHidden" />
@ -78,7 +78,8 @@
<activity android:name=".ui.audio.AudioPlayActivity" /> <activity android:name=".ui.audio.AudioPlayActivity" />
<activity android:name=".ui.explore.ExploreShowActivity" /> <activity android:name=".ui.explore.ExploreShowActivity" />
<activity android:name=".ui.rss.source.manage.RssSourceActivity" /> <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.CheckSourceService" />
<service android:name=".service.DownloadService" /> <service android:name=".service.DownloadService" />
<service android:name=".service.UpdateService" /> <service android:name=".service.UpdateService" />

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

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

@ -5,7 +5,7 @@ object RSSKeywords {
const val RSS_ITEM = "item" const val RSS_ITEM = "item"
const val RSS_ITEM_TITLE = "title" const val RSS_ITEM_TITLE = "title"
const val RSS_ITEM_LINK = "link" 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_CATEGORY = "category"
const val RSS_ITEM_THUMBNAIL = "media:thumbnail" const val RSS_ITEM_THUMBNAIL = "media:thumbnail"
const val RSS_ITEM_ENCLOSURE = "enclosure" const val RSS_ITEM_ENCLOSURE = "enclosure"

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

@ -24,6 +24,9 @@ interface BookDao {
@Query("SELECT bookUrl FROM books WHERE `group` = :group") @Query("SELECT bookUrl FROM books WHERE `group` = :group")
fun observeUrlsByGroup(group: Int): LiveData<List<String>> 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)") @Query("SELECT * FROM books WHERE `name` in (:names)")
fun findByName(vararg names: String): List<Book> fun findByName(vararg names: String): List<Book>

@ -1,10 +1,7 @@
package io.legado.app.data.dao package io.legado.app.data.dao
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.*
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
@Dao @Dao
@ -17,5 +14,11 @@ interface BookGroupDao {
val maxId: Int val maxId: Int
@Insert(onConflict = OnConflictStrategy.REPLACE) @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") @Query("select * from book_sources order by customOrder asc")
fun liveDataAll(): LiveData<List<BookSource>> 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>> 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") @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 <> ''") @Query("select bookSourceGroup from book_sources where bookSourceGroup is not null and bookSourceGroup <> ''")
fun liveGroup(): LiveData<List<String>> 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> fun searchIsEnable(searchKey: String = ""): List<Boolean>
@Query("update book_sources set enabled = 1 where bookSourceUrl in (:sourceUrls)") @Query("update book_sources set enabled = 1 where bookSourceUrl in (:sourceUrls)")
@ -65,6 +65,12 @@ interface BookSourceDao {
@Delete @Delete
fun delete(vararg bookSource: BookSource) 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") @get:Query("select min(customOrder) from book_sources")
val minOrder: Int 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 @Dao
interface RssSourceDao { interface RssSourceDao {
@Query("select * from rssSources where sourceUrl = :key")
fun getByKey(key: String): RssSource?
@get:Query("SELECT * FROM rssSources") @get:Query("SELECT * FROM rssSources")
val all: List<RssSource> val all: List<RssSource>
@Query("SELECT * FROM rssSources") @Query("SELECT * FROM rssSources order by customOrder")
fun liveAll(): LiveData<List<RssSource>> 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>> 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) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rssSource: RssSource) fun insert(vararg rssSource: RssSource)
@Update @Update
fun update(vararg rssSource: RssSource) fun update(vararg rssSource: RssSource)
@Delete
fun delete(vararg rssSource: RssSource)
@Query("delete from rssSources where sourceUrl = :sourceUrl") @Query("delete from rssSources where sourceUrl = :sourceUrl")
fun delete(sourceUrl: String) fun delete(sourceUrl: String)
} }

@ -1,6 +1,6 @@
package io.legado.app.data.dao package io.legado.app.data.dao
import androidx.paging.DataSource import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
import io.legado.app.data.entities.SearchKeyword import io.legado.app.data.entities.SearchKeyword
@ -9,16 +9,19 @@ import io.legado.app.data.entities.SearchKeyword
interface SearchKeywordDao { interface SearchKeywordDao {
@Query("SELECT * FROM search_keywords ORDER BY usage DESC") @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") @Query("SELECT * FROM search_keywords ORDER BY lastUseTime DESC")
fun observeByTime(): DataSource.Factory<Int, SearchKeyword> fun liveDataByTime(): LiveData<List<SearchKeyword>>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Query("SELECT * FROM search_keywords where word like '%'||:key||'%' ORDER BY usage DESC")
fun insert(vararg keywords: SearchKeyword) fun liveDataSearch(key: String): LiveData<List<SearchKeyword>>
@Query("select * from search_keywords where word = :key")
fun get(key: String): SearchKeyword?
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(keyword: SearchKeyword): Long fun insert(vararg keywords: SearchKeyword)
@Update @Update
fun update(vararg keywords: SearchKeyword) 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 package io.legado.app.data.entities
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@Entity(tableName = "rssArticles") @Entity(tableName = "rssArticles")
data class RssArticle( data class RssArticle(
var origin: String = "", var origin: String = "",
var time: Long = System.currentTimeMillis(),
@PrimaryKey @PrimaryKey
var guid: String? = null, var guid: String = "",
var title: String? = null, var title: String? = null,
var author: String? = null, var author: String? = null,
var link: String? = null, var link: String? = null,
@ -16,5 +18,12 @@ data class RssArticle(
var description: String? = null, var description: String? = null,
var content: String? = null, var content: String? = null,
var image: 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 android.os.Parcelable
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@Parcelize @Parcelize
@Entity(tableName = "rssSources") @Entity(tableName = "rssSources", indices = [(Index(value = ["sourceUrl"], unique = false))])
data class RssSource( data class RssSource(
var sourceName: String,
@PrimaryKey @PrimaryKey
var sourceUrl: String, var sourceUrl: String = "",
var iconUrl: String, var sourceName: String = "",
var sourceIcon: String = "",
var sourceGroup: String? = null,
var enabled: Boolean = true, var enabled: Boolean = true,
//列表规则
var ruleArticles: String? = null,
var ruleGuid: String? = null, var ruleGuid: String? = null,
var ruleTitle: String? = null, var ruleTitle: String? = null,
var ruleAuthor: String? = null, var ruleAuthor: String? = null,
var ruleLink: String? = null,
var rulePubDate: String? = null, var rulePubDate: String? = null,
//类别
var ruleCategories: String? = null,
//描述
var ruleDescription: String? = null, var ruleDescription: String? = null,
var ruleContent: String? = null,
var ruleImage: String? = null, var ruleImage: String? = null,
var ruleCategories: String? = null, var ruleContent: String? = null,
var ruleLink: String? = null,
var customOrder: Int = 0 var customOrder: Int = 0
) : Parcelable ) : Parcelable

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

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

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

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

@ -39,8 +39,8 @@ object WebDavHelp {
val url = getWebDavUrl() val url = getWebDavUrl()
val names = arrayListOf<String>() val names = arrayListOf<String>()
if (!url.isNullOrBlank() && initWebDav()) { if (!url.isNullOrBlank() && initWebDav()) {
val files = WebDav(url + "legado/").listFiles() var files = WebDav(url + "legado/").listFiles()
files.reversed() files = files.reversed()
for (index: Int in 0 until min(10, files.size)) { for (index: Int in 0 until min(10, files.size)) {
files[index].displayName?.let { files[index].displayName?.let {
names.add(it) 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.BookInfo
import io.legado.app.model.webbook.BookList import io.legado.app.model.webbook.BookList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext
class WebBook(val bookSource: BookSource) { 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) fun searchBook(
: Coroutine<List<SearchBook>> { key: String,
return Coroutine.async(scope) { page: Int? = 1,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<List<SearchBook>> {
return Coroutine.async(scope, context) {
bookSource.searchUrl?.let { searchUrl -> bookSource.searchUrl?.let { searchUrl ->
val analyzeUrl = AnalyzeUrl( val analyzeUrl = AnalyzeUrl(
ruleUrl = searchUrl, ruleUrl = searchUrl,
@ -40,9 +46,13 @@ class WebBook(val bookSource: BookSource) {
/** /**
* 发现 * 发现
*/ */
fun exploreBook(url: String, page: Int? = 1, scope: CoroutineScope = Coroutine.DEFAULT) fun exploreBook(
: Coroutine<List<SearchBook>> { url: String,
return Coroutine.async(scope) { page: Int? = 1,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<List<SearchBook>> {
return Coroutine.async(scope, context) {
val analyzeUrl = AnalyzeUrl( val analyzeUrl = AnalyzeUrl(
ruleUrl = url, ruleUrl = url,
page = page, page = page,
@ -57,8 +67,12 @@ class WebBook(val bookSource: BookSource) {
/** /**
* 书籍信息 * 书籍信息
*/ */
fun getBookInfo(book: Book, scope: CoroutineScope = Coroutine.DEFAULT): Coroutine<Book> { fun getBookInfo(
return Coroutine.async(scope) { book: Book,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<Book> {
return Coroutine.async(scope, context) {
val analyzeUrl = AnalyzeUrl( val analyzeUrl = AnalyzeUrl(
book = book, book = book,
ruleUrl = book.bookUrl, ruleUrl = book.bookUrl,
@ -76,9 +90,10 @@ class WebBook(val bookSource: BookSource) {
*/ */
fun getChapterList( fun getChapterList(
book: Book, book: Book,
scope: CoroutineScope = Coroutine.DEFAULT scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<List<BookChapter>> { ): Coroutine<List<BookChapter>> {
return Coroutine.async(scope) { return Coroutine.async(scope, context) {
val analyzeUrl = AnalyzeUrl( val analyzeUrl = AnalyzeUrl(
book = book, book = book,
ruleUrl = book.tocUrl, ruleUrl = book.tocUrl,
@ -97,9 +112,10 @@ class WebBook(val bookSource: BookSource) {
book: Book, book: Book,
bookChapter: BookChapter, bookChapter: BookChapter,
nextChapterUrl: String? = null, nextChapterUrl: String? = null,
scope: CoroutineScope = Coroutine.DEFAULT scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<String> { ): Coroutine<String> {
return Coroutine.async(scope) { return Coroutine.async(scope, context) {
val analyzeUrl = val analyzeUrl =
AnalyzeUrl( AnalyzeUrl(
book = book, book = book,

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

@ -11,7 +11,7 @@ import java.io.StringReader
object RssParser { object RssParser {
@Throws(XmlPullParserException::class, IOException::class) @Throws(XmlPullParserException::class, IOException::class)
fun parseXML(xml: String): MutableList<RssArticle> { fun parseXML(xml: String, sourceUrl: String): MutableList<RssArticle> {
val articleList = mutableListOf<RssArticle>() val articleList = mutableListOf<RssArticle>()
var currentArticle = RssArticle() var currentArticle = RssArticle()
@ -42,7 +42,7 @@ object RssParser {
xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_AUTHOR, true) -> xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_AUTHOR, true) ->
if (insideItem) currentArticle.author = xmlPullParser.nextText().trim() if (insideItem) currentArticle.author = xmlPullParser.nextText().trim()
xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_CATEGORY, true) -> 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) -> xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_THUMBNAIL, true) ->
if (insideItem) currentArticle.image = if (insideItem) currentArticle.image =
xmlPullParser.getAttributeValue(null, RSSKeywords.RSS_ITEM_URL) xmlPullParser.getAttributeValue(null, RSSKeywords.RSS_ITEM_URL)
@ -92,6 +92,8 @@ object RssParser {
) { ) {
// The item is correctly parsed // The item is correctly parsed
insideItem = false insideItem = false
currentArticle.categories = currentArticle.categoryList.joinToString(",")
currentArticle.origin = sourceUrl
articleList.add(currentArticle) articleList.add(currentArticle)
currentArticle = RssArticle() 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.constant.Bus
import io.legado.app.help.ActivityHelp import io.legado.app.help.ActivityHelp
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.utils.LogUtils
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
@ -22,7 +23,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
fun handleIntent(context: Context, intent: Intent): Boolean { fun handleIntent(context: Context, intent: Intent): Boolean {
val intentAction = intent.action val intentAction = intent.action
val keyEventAction = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.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 (Intent.ACTION_MEDIA_BUTTON == intentAction) {
if (keyEventAction == KeyEvent.ACTION_DOWN) { if (keyEventAction == KeyEvent.ACTION_DOWN) {
readAloud(context) readAloud(context)

@ -24,7 +24,7 @@ class AboutFragment : PreferenceFragmentCompat() {
override fun onPreferenceTreeClick(preference: Preference?): Boolean { override fun onPreferenceTreeClick(preference: Preference?): Boolean {
when (preference?.key) { when (preference?.key) {
"mail" -> openIntent(Intent.ACTION_SENDTO, "mailto:kunfei.ge@gmail.com") "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) return super.onPreferenceTreeClick(preference)
} }

@ -8,11 +8,10 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity import io.legado.app.base.BaseActivity
import io.legado.app.lib.theme.ATH 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.activity_donate.*
import kotlinx.android.synthetic.main.view_title_bar.* import kotlinx.android.synthetic.main.view_title_bar.*
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
@ -60,7 +59,7 @@ class DonateActivity : BaseActivity(R.layout.activity_donate) {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} finally { } 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.lib.theme.ATH
import io.legado.app.ui.book.info.edit.BookInfoEditActivity import io.legado.app.ui.book.info.edit.BookInfoEditActivity
import io.legado.app.ui.book.read.ReadBookActivity 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.ui.changesource.ChangeSourceDialog
import io.legado.app.utils.getCompatDrawable import io.legado.app.utils.getCompatDrawable
import io.legado.app.utils.getViewModel 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.getDisplayIntro() // getString(R.string.intro_show, book.getDisplayIntro())
book.getDisplayCover()?.let { book.getDisplayCover()?.let {
ImageLoader.load(this, it) ImageLoader.load(this, it)
.placeholder(R.drawable.img_cover_default) .placeholder(R.drawable.image_cover_default)
.error(R.drawable.img_cover_default) .error(R.drawable.image_cover_default)
.centerCrop() .centerCrop()
.setAsDrawable(iv_cover) .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 { tv_origin.onClick {
viewModel.bookData.value?.let { viewModel.bookData.value?.let {
startActivity<SourceEditActivity>(Pair("data", it.origin)) startActivity<BookSourceEditActivity>(Pair("data", it.origin))
} }
} }
tv_change_source.onClick { tv_change_source.onClick {
@ -204,8 +208,10 @@ class BookInfoActivity : VMBaseActivity<BookInfoViewModel>(R.layout.activity_boo
} }
} }
iv_chapter_top.onClick { iv_chapter_top.onClick {
adapter.reorder = !adapter.reorder rv_chapter_list.scrollToPosition(0)
adapter.notifyDataSetChanged() }
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? { override val curOrigin: String?
return viewModel.bookData.value?.origin get() = viewModel.bookData.value?.origin
}
override fun oldBook(): Book? { override val oldBook: Book?
return viewModel.bookData.value get() = viewModel.bookData.value
}
override fun changeTo(book: Book) { override fun changeTo(book: Book) {
upLoading(true) upLoading(true)

@ -99,7 +99,6 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
} }
}.onError { }.onError {
toast(R.string.error_get_chapter_list) toast(R.string.error_get_chapter_list)
it.printStackTrace()
} }
} ?: toast(R.string.error_no_source) } ?: toast(R.string.error_no_source)
} }
@ -130,7 +129,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
chapters chapters
) )
book.durChapterTitle = chapters[book.durChapterIndex].title book.durChapterTitle = chapters[book.durChapterIndex].title
App.db.bookDao().update(book) App.db.bookDao().insert(book)
App.db.bookChapterDao().insert(*chapters.toTypedArray()) App.db.bookChapterDao().insert(*chapters.toTypedArray())
bookData.postValue(book) bookData.postValue(book)
chapterListData.postValue(chapters) 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.*
import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR 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.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.changesource.ChangeSourceDialog
import io.legado.app.ui.chapterlist.ChapterListActivity import io.legado.app.ui.chapterlist.ChapterListActivity
import io.legado.app.ui.replacerule.ReplaceRuleActivity 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.ChapterProvider
import io.legado.app.ui.widget.page.PageView import io.legado.app.ui.widget.page.PageView
import io.legado.app.ui.widget.page.TextChapter import io.legado.app.ui.widget.page.TextChapter
import io.legado.app.ui.widget.page.delegate.PageDelegate import io.legado.app.ui.widget.page.delegate.PageDelegate
import io.legado.app.utils.* 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_book_page.*
import kotlinx.android.synthetic.main.view_read_menu.* import kotlinx.android.synthetic.main.view_read_menu.*
import kotlinx.android.synthetic.main.view_title_bar.* 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.startActivityForResult
import org.jetbrains.anko.toast 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, PageView.CallBack,
ReadMenu.CallBack, ReadMenu.CallBack,
ReadAloudDialog.CallBack, ReadAloudDialog.CallBack,
@ -113,7 +114,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_rea
private fun initView() { private fun initView() {
tv_chapter_name.onClick { tv_chapter_name.onClick {
viewModel.webBook?.let { viewModel.webBook?.let {
startActivityForResult<SourceEditActivity>( startActivityForResult<BookSourceEditActivity>(
requestCodeEditSource, requestCodeEditSource,
Pair("data", it.bookSource.bookSourceUrl) 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.text = it.url
tv_chapter_url.visible() 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_pre.isEnabled = viewModel.durChapterIndex != 0
tv_next.isEnabled = viewModel.durChapterIndex != viewModel.chapterSize - 1 tv_next.isEnabled = viewModel.durChapterIndex != viewModel.chapterSize - 1
curPageChanged() curPageChanged()
@ -312,6 +313,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_rea
} }
private fun curPageChanged() { private fun curPageChanged() {
seek_read_page.progress = viewModel.durPageIndex
when (readAloudStatus) { when (readAloudStatus) {
Status.PLAY -> readAloud() Status.PLAY -> readAloud()
Status.PAUSE -> { Status.PAUSE -> {
@ -328,13 +330,11 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_rea
return viewModel.chapterSize return viewModel.chapterSize
} }
override fun curOrigin(): String? { override val curOrigin: String?
return viewModel.bookData.value?.origin get() = viewModel.bookData.value?.origin
}
override fun oldBook(): Book? { override val oldBook: Book?
return viewModel.bookData.value get() = viewModel.bookData.value
}
override fun changeTo(book: Book) { override fun changeTo(book: Book) {
viewModel.changeTo(book) viewModel.changeTo(book)
@ -608,7 +608,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_rea
} }
} }
observeEvent<String>(Bus.REPLACE) { observeEvent<String>(Bus.REPLACE) {
toast(it) ReplaceEditDialog().show(supportFragmentManager, "replaceEditDialog")
} }
} }

@ -68,7 +68,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} }
} }
saveRead(book) saveRead(book)
}.onError { it.printStackTrace() } }
} }
private fun loadBookInfo( private fun loadBookInfo(
@ -103,7 +103,6 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} }
}?.onError { }?.onError {
toast(R.string.error_load_toc) toast(R.string.error_load_toc)
it.printStackTrace()
} ?: autoChangeSource() } ?: 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 { private fun brightnessAuto(): Boolean {
return context.getPrefBoolean("brightnessAuto", true) 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.book.read.ReadBookActivity
import io.legado.app.ui.widget.font.FontSelectDialog import io.legado.app.ui.widget.font.FontSelectDialog
import io.legado.app.utils.* 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 kotlinx.android.synthetic.main.dialog_read_book_style.*
import org.jetbrains.anko.sdk27.listeners.onCheckedChange import org.jetbrains.anko.sdk27.listeners.onCheckedChange
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
@ -249,7 +249,7 @@ class ReadStyleDialog : DialogFragment() {
ReadBookConfig.getConfig(i).apply { ReadBookConfig.getConfig(i).apply {
when (bgType()) { when (bgType()) {
2 -> ImageLoader.load(requireContext(), bgStr()).centerCrop().setAsDrawable(iv) 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.LivePagedListBuilder
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity 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.data.entities.SearchShow
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.primaryTextColor import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.gone
import io.legado.app.utils.invisible import io.legado.app.utils.invisible
import io.legado.app.utils.visible 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 kotlinx.android.synthetic.main.view_search.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.startActivity 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 override val viewModel: SearchViewModel
get() = getViewModel(SearchViewModel::class.java) get() = getViewModel(SearchViewModel::class.java)
private lateinit var adapter: SearchAdapter private lateinit var adapter: SearchAdapter
private lateinit var bookAdapter: BookAdapter
private lateinit var historyKeyAdapter: HistoryKeyAdapter
private var searchBookData: LiveData<PagedList<SearchShow>>? = null 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?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
initRecyclerView() initRecyclerView()
initSearchView() initSearchView()
initOtherView()
initData() initData()
intent.getStringExtra("key")?.let { intent.getStringExtra("key")?.let {
search_view.setQuery(it, true) search_view.setQuery(it, true)
@ -49,6 +61,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_search)
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
search_view.clearFocus() search_view.clearFocus()
query?.let { query?.let {
viewModel.saveSearchKey(query)
viewModel.search(it, { viewModel.search(it, {
refresh_progress_bar.isAutoLoading = true refresh_progress_bar.isAutoLoading = true
initData() initData()
@ -63,19 +76,43 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_search)
override fun onQueryTextChange(newText: String?): Boolean { override fun onQueryTextChange(newText: String?): Boolean {
if (newText.isNullOrBlank()) viewModel.stop() if (newText.isNullOrBlank()) viewModel.stop()
upHistory(newText)
return false return false
} }
}) })
fb_stop.onClick { viewModel.stop() } search_view.setOnQueryTextFocusChangeListener { _, hasFocus ->
if (hasFocus) {
ll_history.visible()
} else {
ll_history.invisible()
}
}
} }
private fun initRecyclerView() { private fun initRecyclerView() {
ATH.applyEdgeEffectColor(recycler_view) 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) adapter = SearchAdapter(this)
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.adapter = adapter 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() { private fun initData() {
searchBookData?.removeObservers(this) searchBookData?.removeObservers(this)
searchBookData = LivePagedListBuilder( searchBookData = LivePagedListBuilder(
@ -85,6 +122,35 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_search)
), 30 ), 30
).build() ).build()
searchBookData?.observe(this, Observer { adapter.submitList(it) }) 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) { 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 { searchBook.coverUrl.let {
ImageLoader.load(context, it)//Glide自动识别http://和file:// ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.img_cover_default) .placeholder(R.drawable.image_cover_default)
.error(R.drawable.img_cover_default) .error(R.drawable.image_cover_default)
.centerCrop() .centerCrop()
.setAsDrawable(iv_cover) .setAsDrawable(iv_cover)
} }
@ -100,8 +100,8 @@ class SearchAdapter(val callBack: CallBack) :
1 -> bv_originCount.setBadgeCount(searchBook.originCount) 1 -> bv_originCount.setBadgeCount(searchBook.originCount)
2 -> searchBook.coverUrl.let { 2 -> searchBook.coverUrl.let {
ImageLoader.load(context, it)//Glide自动识别http://和file:// ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.img_cover_default) .placeholder(R.drawable.image_cover_default)
.error(R.drawable.img_cover_default) .error(R.drawable.image_cover_default)
.centerCrop() .centerCrop()
.setAsDrawable(iv_cover) .setAsDrawable(iv_cover)
} }

@ -4,12 +4,15 @@ import android.app.Application
import io.legado.app.App import io.legado.app.App
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.SearchBook 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.help.coroutine.Coroutine
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
class SearchViewModel(application: Application) : BaseViewModel(application) { class SearchViewModel(application: Application) : BaseViewModel(application) {
private var searchPool = Executors.newFixedThreadPool(16).asCoroutineDispatcher()
private var task: Coroutine<*>? = null private var task: Coroutine<*>? = null
var searchKey: String = "" var searchKey: String = ""
var startTime: Long = 0 var startTime: Long = 0
@ -30,7 +33,12 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
val bookSourceList = App.db.bookSourceDao().allEnabled val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) { for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute) //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) .timeout(30000L)
.onSuccess(Dispatchers.IO) { .onSuccess(Dispatchers.IO) {
it?.let { list -> it?.let { list ->
@ -40,10 +48,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
} }
} }
} }
delay(100)//每隔100毫秒搜索一个书源
} }
}.onError {
it.printStackTrace()
} }
task?.invokeOnCompletion { task?.invokeOnCompletion {
@ -61,4 +66,24 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
success?.invoke(searchBook) 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.lib.theme.accentColor
import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.ui.qrcode.QrCodeActivity
import io.legado.app.utils.getViewModel 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.android.synthetic.main.view_search.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.anko.startActivityForResult import org.jetbrains.anko.startActivityForResult
import org.jetbrains.anko.toast 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 override val viewModel: BookSourceDebugModel
get() = getViewModel(SourceDebugModel::class.java) get() = getViewModel(BookSourceDebugModel::class.java)
private lateinit var adapter: SourceDebugAdapter private lateinit var adapter: BookSourceDebugAdapter
private val qrRequestCode = 101 private val qrRequestCode = 101
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -42,7 +43,7 @@ class SourceDebugActivity : VMBaseActivity<SourceDebugModel>(R.layout.activity_b
private fun initRecyclerView() { private fun initRecyclerView() {
ATH.applyEdgeEffectColor(recycler_view) ATH.applyEdgeEffectColor(recycler_view)
adapter = SourceDebugAdapter(this) adapter = BookSourceDebugAdapter(this)
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.adapter = adapter recycler_view.adapter = adapter
rotate_loading.loadingColor = accentColor rotate_loading.loadingColor = accentColor

@ -6,7 +6,7 @@ import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import kotlinx.android.synthetic.main.item_log.view.* import kotlinx.android.synthetic.main.item_log.view.*
class SourceDebugAdapter(context: Context) : class BookSourceDebugAdapter(context: Context) :
SimpleRecyclerAdapter<String>(context, R.layout.item_log) { SimpleRecyclerAdapter<String>(context, R.layout.item_log) {
override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList<Any>) {
holder.itemView.apply { 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
import io.legado.app.model.webbook.SourceDebug 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 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 android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.EditEntity
import kotlinx.android.synthetic.main.item_source_edit.view.* 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) { set(value) {
field = value field = value
notifyDataSetChanged() notifyDataSetChanged()
@ -34,7 +35,7 @@ class SourceEditAdapter : RecyclerView.Adapter<SourceEditAdapter.MyViewHolder>()
} }
class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) { 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) { if (editText.getTag(R.id.tag1) == null) {
val listener = object : View.OnAttachStateChangeListener { val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) { override fun onViewAttachedToWindow(v: View) {

@ -3,29 +3,44 @@ package io.legado.app.ui.book.source.edit
import android.app.Application import android.app.Application
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.App import io.legado.app.App
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.storage.OldRule import io.legado.app.help.storage.OldRule
class SourceEditViewModel(application: Application) : BaseViewModel(application) { class BookSourceEditViewModel(application: Application) : BaseViewModel(application) {
val sourceLiveData: MutableLiveData<BookSource> = MutableLiveData() val sourceLiveData: MutableLiveData<BookSource> = MutableLiveData()
var oldSourceUrl: String? = null
fun setBookSource(key: String) { fun initData(intent: Intent) {
execute { 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(it)
} ?: sourceLiveData.postValue(BookSource()) } ?: let {
sourceLiveData.postValue(BookSource().apply {
customOrder = App.db.bookSourceDao().maxOrder + 1
})
}
} }
} }
fun save(bookSource: BookSource, finally: (() -> Unit)? = null) { fun save(bookSource: BookSource, finally: (() -> Unit)? = null) {
execute { execute {
if (bookSource.customOrder == 0) { oldSourceUrl?.let {
bookSource.customOrder = App.db.bookSourceDao().allCount() if (oldSourceUrl != bookSource.bookSourceUrl) {
App.db.bookSourceDao().delete(it)
}
} }
oldSourceUrl = bookSource.bookSourceUrl
App.db.bookSourceDao().insert(bookSource) App.db.bookSourceDao().insert(bookSource)
}.onFinally { }.onFinally {
finally?.let { it() } 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.primaryTextColor
import io.legado.app.lib.theme.view.ATEAutoCompleteTextView import io.legado.app.lib.theme.view.ATEAutoCompleteTextView
import io.legado.app.service.CheckSourceService 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.ui.qrcode.QrCodeActivity
import io.legado.app.utils.ACache import io.legado.app.utils.ACache
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
@ -78,7 +78,7 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_add_book_source -> { R.id.menu_add_book_source -> {
this.startActivity<SourceEditActivity>() this.startActivity<BookSourceEditActivity>()
} }
R.id.menu_import_book_source_qr -> { R.id.menu_import_book_source_qr -> {
this.startActivityForResult<QrCodeActivity>(qrRequestCode) this.startActivityForResult<QrCodeActivity>(qrRequestCode)
@ -151,12 +151,8 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
} }
bookSourceLiveDate?.observe(this, Observer { bookSourceLiveDate?.observe(this, Observer {
search_view.queryHint = getString(R.string.search_book_source_num, it.size) search_view.queryHint = getString(R.string.search_book_source_num, it.size)
val diffResult = DiffUtil.calculateDiff( val diffResult = DiffUtil
DiffCallBack( .calculateDiff(DiffCallBack(adapter.getItems(), it))
adapter.getItems(),
it
)
)
adapter.setItemsNoNotify(it) adapter.setItemsNoNotify(it)
diffResult.dispatchUpdatesTo(adapter) diffResult.dispatchUpdatesTo(adapter)
}) })
@ -232,7 +228,7 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
} }
override fun edit(bookSource: BookSource) { override fun edit(bookSource: BookSource) {
startActivity<SourceEditActivity>(Pair("data", bookSource.bookSourceUrl)) startActivity<BookSourceEditActivity>(Pair("data", bookSource.bookSourceUrl))
} }
override fun upOrder() { override fun upOrder() {

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

@ -29,7 +29,6 @@ import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_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 kotlinx.android.synthetic.main.item_group_manage.view.*
import org.jetbrains.anko.sdk27.listeners.onClick 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 viewModel: ChangeSourceViewModel
private lateinit var changeSourceAdapter: ChangeSourceAdapter private lateinit var changeSourceAdapter: ChangeSourceAdapter
@ -49,6 +50,7 @@ class ChangeSourceDialog : DialogFragment(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
callBack = activity as? CallBack
viewModel.searchStateData.observe(viewLifecycleOwner, Observer { viewModel.searchStateData.observe(viewLifecycleOwner, Observer {
refresh_progress_bar.isAutoLoading = it refresh_progress_bar.isAutoLoading = it
}) })
@ -114,31 +116,24 @@ class ChangeSourceDialog : DialogFragment(),
} }
override fun changeTo(searchBook: SearchBook) { override fun changeTo(searchBook: SearchBook) {
val activity = activity val book = searchBook.toBook()
if (activity is CallBack) { callBack?.oldBook?.let { oldBook ->
val book = searchBook.toBook() book.durChapterIndex = oldBook.durChapterIndex
activity.oldBook()?.let { oldBook -> book.durChapterPos = oldBook.durChapterPos
book.durChapterIndex = oldBook.durChapterIndex book.durChapterTitle = oldBook.durChapterTitle
book.durChapterPos = oldBook.durChapterPos book.customCoverUrl = oldBook.customCoverUrl
book.durChapterTitle = oldBook.durChapterTitle book.customIntro = oldBook.customIntro
book.customCoverUrl = oldBook.customCoverUrl book.order = oldBook.order
book.customIntro = oldBook.customIntro if (book.coverUrl.isNullOrEmpty()) {
book.order = oldBook.order book.coverUrl = oldBook.getDisplayCover()
if (book.coverUrl.isNullOrEmpty()) {
book.coverUrl = oldBook.getDisplayCover()
}
activity.changeTo(book)
} }
callBack?.changeTo(book)
} }
dismiss() dismiss()
} }
override fun curOrigin(): String { override fun curOrigin(): String {
val activity = activity return callBack?.curOrigin ?: ""
if (activity is CallBack) {
return activity.curOrigin() ?: ""
}
return ""
} }
override fun adapter(): ChangeSourceAdapter { override fun adapter(): ChangeSourceAdapter {
@ -146,8 +141,8 @@ class ChangeSourceDialog : DialogFragment(),
} }
interface CallBack { interface CallBack {
fun curOrigin(): String? val curOrigin: String?
fun oldBook(): Book? val oldBook: Book?
fun changeTo(book: Book) fun changeTo(book: Book)
} }

@ -12,11 +12,13 @@ import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.anko.debug import org.jetbrains.anko.debug
import java.util.concurrent.Executors
class ChangeSourceViewModel(application: Application) : BaseViewModel(application) { class ChangeSourceViewModel(application: Application) : BaseViewModel(application) {
private var searchPool = Executors.newFixedThreadPool(16).asCoroutineDispatcher()
var callBack: CallBack? = null var callBack: CallBack? = null
val searchStateData = MutableLiveData<Boolean>() val searchStateData = MutableLiveData<Boolean>()
var name: String = "" var name: String = ""
@ -56,7 +58,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
val bookSourceList = App.db.bookSourceDao().allEnabled val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) { for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute) //task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(name, scope = this@execute) WebBook(item).searchBook(name, scope = this@execute, context = searchPool)
.timeout(30000L) .timeout(30000L)
.onSuccess(IO) { .onSuccess(IO) {
it?.forEach { searchBook -> 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 { interface CallBack {
fun adapter(): ChangeSourceAdapter 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.R
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.utils.LogUtils
import io.legado.app.utils.getPrefString import io.legado.app.utils.getPrefString
@ -39,6 +40,7 @@ class ConfigFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChange
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) { when (key) {
"downloadPath" -> BookHelp.upDownloadPath() "downloadPath" -> BookHelp.upDownloadPath()
"recordLog" -> LogUtils.upLevel()
} }
} }

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

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

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

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

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

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

@ -7,17 +7,28 @@ import io.legado.app.data.entities.BookGroup
class BookshelfViewModel(application: Application) : BaseViewModel(application) { class BookshelfViewModel(application: Application) : BaseViewModel(application) {
fun saveBookGroup(group: String?) {
if (!group.isNullOrBlank()) { fun addGroup(groupName: String) {
execute { execute {
App.db.bookGroupDao().insert( val maxId = App.db.bookGroupDao().maxId
BookGroup( val bookGroup = BookGroup(
App.db.bookGroupDao().maxId + 1, groupId = maxId.plus(1),
group 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.data.entities.BookSource
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.primaryTextColor 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.ui.explore.ExploreShowActivity
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
@ -88,7 +88,7 @@ class ExploreFragment : VMBaseFragment<ExploreViewModel>(R.layout.fragment_find_
} }
override fun editSource(sourceUrl: String) { override fun editSource(sourceUrl: String) {
startActivity<SourceEditActivity>(Pair("data", sourceUrl)) startActivity<BookSourceEditActivity>(Pair("data", sourceUrl))
} }
override fun toTop(source: BookSource) { override fun toTop(source: BookSource) {

@ -10,6 +10,7 @@ import androidx.preference.PreferenceFragmentCompat
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseFragment 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.Permissions
import io.legado.app.help.permission.PermissionsCompat import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.help.storage.Backup 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.ConfigActivity
import io.legado.app.ui.config.ConfigViewModel import io.legado.app.ui.config.ConfigViewModel
import io.legado.app.ui.replacerule.ReplaceRuleActivity import io.legado.app.ui.replacerule.ReplaceRuleActivity
import io.legado.app.utils.LogUtils
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
import kotlinx.android.synthetic.main.view_title_bar.* import kotlinx.android.synthetic.main.view_title_bar.*
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
@ -82,9 +84,9 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
key: String? key: String?
) { ) {
when (key) { when (key) {
"isNightTheme" -> { "isNightTheme" -> App.INSTANCE.applyDayNight()
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.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.RssSource 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) : class RssAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<RssSource>(context, R.layout.item_rss) { SimpleRecyclerAdapter<RssSource>(context, R.layout.item_rss) {
override fun convert(holder: ItemViewHolder, item: RssSource, payloads: MutableList<Any>) { 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 { interface CallBack {
fun openRss() fun openRss(rssSource: RssSource)
} }
} }

@ -1,15 +1,24 @@
package io.legado.app.ui.main.rss package io.legado.app.ui.main.rss
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.Gravity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.EditText
import android.widget.LinearLayout
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseFragment 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.lib.theme.ATH
import io.legado.app.ui.rss.article.RssArticlesActivity
import io.legado.app.ui.rss.source.manage.RssSourceActivity import io.legado.app.ui.rss.source.manage.RssSourceActivity
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
import kotlinx.android.synthetic.main.fragment_rss.* import kotlinx.android.synthetic.main.fragment_rss.*
@ -34,6 +43,25 @@ class RssFragment : BaseFragment(R.layout.fragment_rss),
super.onCompatOptionsItemSelected(item) super.onCompatOptionsItemSelected(item)
when (item.itemId) { when (item.itemId) {
R.id.menu_rss_config -> startActivity<RssSourceActivity>() 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 io.legado.app.utils.splitNotBlank
import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_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 kotlinx.android.synthetic.main.item_group_manage.view.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick

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

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

@ -1,3 +1,70 @@
package io.legado.app.ui.rss.article 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 android.os.Bundle
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity import io.legado.app.base.BaseActivity
import kotlinx.android.synthetic.main.activity_rss_read.*
class ReadRssActivity : BaseActivity(R.layout.activity_rss_read) {
class ReadRssActivity : BaseActivity(R.layout.activity_read_rss) {
override fun onActivityCreated(savedInstanceState: Bundle?) { 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 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 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.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.R
import io.legado.app.base.VMBaseActivity 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 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 : 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 override val viewModel: RssSourceEditViewModel
get() = getViewModel(RssSourceEditViewModel::class.java) get() = getViewModel(RssSourceEditViewModel::class.java)
override fun onActivityCreated(savedInstanceState: Bundle?) { 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 package io.legado.app.ui.rss.source.edit
import android.app.Application 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.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) { 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.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.SubMenu
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat 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.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.RssSource 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.lib.theme.primaryTextColor
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.utils.getViewModel 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.activity_rss_source.*
import kotlinx.android.synthetic.main.view_search.* import kotlinx.android.synthetic.main.view_search.*
import kotlinx.android.synthetic.main.view_title_bar.* 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) get() = getViewModel(RssSourceViewModel::class.java)
private lateinit var adapter: RssSourceAdapter 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?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
initRecyclerView() initRecyclerView()
initSearchView() initSearchView()
initLiveDataGroup()
initLiveDataSource()
} }
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
@ -41,9 +52,20 @@ class RssSourceActivity : VMBaseActivity<RssSourceViewModel>(R.layout.activity_r
return super.onCompatCreateOptionsMenu(menu) 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 { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { 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) 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) { override fun toTop(source: RssSource) {
viewModel.topSource(source)
} }
override fun upOrder() { override fun upOrder() {
viewModel.upOrder()
} }
} }

@ -1,18 +1,95 @@
package io.legado.app.ui.rss.source.manage package io.legado.app.ui.rss.source.manage
import android.content.Context import android.content.Context
import android.view.Menu
import android.widget.PopupMenu
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.help.ItemTouchCallback 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) : class RssSourceAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<RssSource>(context, R.layout.item_rss_source), SimpleRecyclerAdapter<RssSource>(context, R.layout.item_rss_source),
ItemTouchCallback.OnItemTouchCallbackListener { 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 package io.legado.app.ui.rss.source.manage
import android.app.Application import android.app.Application
import io.legado.app.App
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.RssSource
class RssSourceViewModel(application: Application) : BaseViewModel(application) { 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( class KeyboardToolPop(
context: Context, context: Context,
private val chars: List<String>, private val chars: List<String>,
val onClickListener: OnClickListener? val callBack: CallBack?
) : PopupWindow(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) { ) : PopupWindow(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {
init { init {
@ -45,13 +45,13 @@ class KeyboardToolPop(
override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList<Any>) {
with(holder.itemView) { with(holder.itemView) {
text_view.text = item text_view.text = item
onClick { onClickListener?.click(item) } onClick { callBack?.sendText(item) }
} }
} }
} }
interface OnClickListener { interface CallBack {
fun click(text: String) fun sendText(text: String)
} }
} }

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

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

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

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

@ -23,7 +23,49 @@ import kotlin.math.min
* 本地缓存 * 本地缓存
*/ */
class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) { 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 { init {
try { try {
@ -48,12 +90,14 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* @param value 保存的String数据 * @param value 保存的String数据
*/ */
fun put(key: String, value: String) { fun put(key: String, value: String) {
try { mCache?.let { mCache ->
val file = mCache.newFile(key) try {
file.writeText(value) val file = mCache.newFile(key)
mCache.put(file) file.writeText(value)
} catch (e: Exception) { mCache.put(file)
e.printStackTrace() } catch (e: Exception) {
e.printStackTrace()
}
} }
} }
@ -74,24 +118,26 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* @return String 数据 * @return String 数据
*/ */
fun getAsString(key: String): String? { fun getAsString(key: String): String? {
val file = mCache[key] mCache?.let { mCache ->
if (!file.exists()) val file = mCache[key]
return null if (!file.exists())
var removeFile = false return null
return try { var removeFile = false
val text = file.readText() try {
if (!Utils.isDue(text)) { val text = file.readText()
Utils.clearDateInfo(text) if (!Utils.isDue(text)) {
} else { return Utils.clearDateInfo(text)
removeFile = true } else {
null removeFile = true
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
if (removeFile)
remove(key)
} }
} catch (e: IOException) {
null
} finally {
if (removeFile)
remove(key)
} }
return null
} }
// ======================================= // =======================================
@ -184,9 +230,11 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* @param value 保存的数据 * @param value 保存的数据
*/ */
fun put(key: String, value: ByteArray) { fun put(key: String, value: ByteArray) {
val file = mCache.newFile(key) mCache?.let { mCache ->
file.writeBytes(value) val file = mCache.newFile(key)
mCache.put(file) file.writeBytes(value)
mCache.put(file)
}
} }
/** /**
@ -206,26 +254,28 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* @return byte 数据 * @return byte 数据
*/ */
fun getAsBinary(key: String): ByteArray? { fun getAsBinary(key: String): ByteArray? {
var removeFile = false mCache?.let { mCache ->
try { var removeFile = false
val file = mCache[key] try {
if (!file.exists()) val file = mCache[key]
return null if (!file.exists())
return null
val byteArray = file.readBytes() val byteArray = file.readBytes()
return if (!Utils.isDue(byteArray)) { return if (!Utils.isDue(byteArray)) {
Utils.clearDateInfo(byteArray) Utils.clearDateInfo(byteArray)
} else { } else {
removeFile = true removeFile = true
null null
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
if (removeFile)
remove(key)
} }
} catch (e: Exception) {
e.printStackTrace()
return null
} finally {
if (removeFile)
remove(key)
} }
return null
} }
/** /**
@ -269,7 +319,6 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
return ois.readObject() return ois.readObject()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return null
} finally { } finally {
try { try {
bais?.close() bais?.close()
@ -371,15 +420,16 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* @return value 缓存的文件 * @return value 缓存的文件
*/ */
fun file(key: String): File? { fun file(key: String): File? {
try { mCache?.let { mCache ->
val f = mCache.newFile(key) try {
if (f.exists()) { val f = mCache.newFile(key)
return f if (f.exists()) {
return f
}
} catch (e: Exception) {
e.printStackTrace()
} }
} catch (e: Exception) {
e.printStackTrace()
} }
return null return null
} }
@ -389,14 +439,14 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* @return 是否移除成功 * @return 是否移除成功
*/ */
fun remove(key: String): Boolean { fun remove(key: String): Boolean {
return mCache.remove(key) return mCache?.remove(key) == true
} }
/** /**
* 清除所有数据 * 清除所有数据
*/ */
fun clear() { 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 { object LogUtils {
const val TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"
private val logger: Logger by lazy { @JvmStatic
Logger.getGlobal().apply { fun d(tag: String, msg: String) {
addFileHandler( logger.log(Level.INFO, "$tag $msg")
this,
Level.INFO,
FileHelp.getCachePath()
)
}
} }
fun log(msg: String) { @JvmStatic
if (App.INSTANCE.getPrefBoolean("recordLog")) { fun e(tag: String, msg: String) {
logger.log(Level.INFO, msg) logger.log(Level.WARNING, "$tag $msg")
}
} }
private const val DATE_PATTERN = "yyyy-MM-dd" private val logger: Logger by lazy {
const val TIME_PATTERN = "HH:mm:ss" Logger.getGlobal().apply {
addHandler(fileHandler)
/** }
* 为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 fileHandler by lazy {
* 为log添加文件输出Handler val logFolder = FileHelp.getCachePath() + File.separator + "logs"
* FileHelp.getFolder(logFolder)
* @param log 要添加文件输出handler的log FileHandler(
* @param level log输出等级 logFolder + File.separator + "app.log",
* @param filePath 指定文件全路径 2048,
*/ 10
fun addFileHandler(log: Logger, level: Level, filePath: String) { ).apply {
var fileHandler: FileHandler? = null formatter = object : Formatter() {
try {
fileHandler =
FileHandler(filePath + File.separator + getCurrentDateStr(DATE_PATTERN) + ".log")
// 设置输出文件的等级(如果FileHandler的等级高于或者等于log的level,则按照FileHandler的level输出到文件,如果低于,则按照Log等级输出)
fileHandler.level = level
fileHandler.formatter = object : Formatter() {
override fun format(record: LogRecord): String { override fun format(record: LogRecord): String {
// 设置文件输出格式 // 设置文件输出格式
return (getCurrentDateStr(TIME_PATTERN) + ":" + record.message + "\n") return (getCurrentDateStr(TIME_PATTERN) + ": " + record.message + "\n")
} }
} }
} catch (e: Exception) { level = if (App.INSTANCE.getPrefBoolean("recordLog")) {
e.printStackTrace() 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_width="86dp"
android:layout_height="120dp" android:layout_height="120dp"
android:layout_margin="10dp" android:layout_margin="10dp"
android:src="@drawable/img_cover_default" android:src="@drawable/image_cover_default"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title_bar" /> app:layout_constraintTop_toBottomOf="@+id/title_bar" />
@ -165,7 +165,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="8dp" android:padding="8dp"
android:text="简介" android:text="@string/book_intro"
android:textColor="@color/tv_text_default" android:textColor="@color/tv_text_default"
android:textSize="16sp" /> android:textSize="16sp" />
@ -192,22 +192,15 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toRightOf="@+id/fg"> app:layout_constraintLeft_toRightOf="@+id/fg">
<LinearLayout <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:paddingLeft="8dp"
android:paddingRight="8dp"
<TextView android:paddingTop="8dp"
android:layout_width="match_parent" android:text="@string/chapter_list"
android:layout_height="wrap_content" android:textColor="@color/tv_text_default"
android:layout_weight="1.0" android:textSize="16sp" />
android:paddingLeft="8dp"
android:paddingTop="8dp"
android:text="目录"
android:textColor="@color/tv_text_default"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/ll_chapter_base_info" android:id="@+id/ll_chapter_base_info"
@ -223,7 +216,7 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="当前位置:" /> android:text="@string/dur_pos" />
<TextView <TextView
android:id="@+id/tv_current_chapter_info" android:id="@+id/tv_current_chapter_info"
@ -245,11 +238,21 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/go_to_top" android:contentDescription="@string/go_to_top"
android:padding="10dp" android:src="@drawable/ic_arrow_drop_up"
android:src="@drawable/ic_exchange_order"
android:tooltipText="@string/go_to_top" android:tooltipText="@string/go_to_top"
app:tint="@color/tv_text_default" app:tint="@color/tv_text_default"
tools:ignore="UnusedAttribute" /> 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>
</LinearLayout> </LinearLayout>

@ -25,7 +25,7 @@
android:layout_height="260dp" android:layout_height="260dp"
android:layout_gravity="center" android:layout_gravity="center"
android:contentDescription="@string/img_cover" 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_constraintTop_toBottomOf="@+id/title_bar"
app:layout_constraintBottom_toTopOf="@+id/ll_player_progress" app:layout_constraintBottom_toTopOf="@+id/ll_player_progress"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"

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

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

@ -36,6 +36,61 @@
</io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout> </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 <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fb_stop" android:id="@+id/fb_stop"
android:layout_width="wrap_content" 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"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/ll_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
@ -11,7 +12,12 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:contentInsetStartWithNavigation="0dp" app:contentInsetStartWithNavigation="0dp"
app:displayHomeAsUp="true" app:displayHomeAsUp="true"
app:fitStatusBar="false"
app:title="@string/rss_source_edit" /> 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> </LinearLayout>

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

Loading…
Cancel
Save