diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c1e691938..1cf753157 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,7 +46,7 @@ android:name=".help.permission.PermissionActivity" android:theme="@style/Activity.Permission" /> @@ -78,7 +78,8 @@ - + + diff --git a/app/src/main/java/io/legado/app/base/BaseViewModel.kt b/app/src/main/java/io/legado/app/base/BaseViewModel.kt index 1aa8452c9..3610a00a5 100644 --- a/app/src/main/java/io/legado/app/base/BaseViewModel.kt +++ b/app/src/main/java/io/legado/app/base/BaseViewModel.kt @@ -2,26 +2,38 @@ package io.legado.app.base import android.app.Application import android.content.Context +import androidx.annotation.CallSuper import androidx.lifecycle.AndroidViewModel import io.legado.app.App import io.legado.app.help.coroutine.Coroutine import kotlinx.coroutines.* import org.jetbrains.anko.AnkoLogger import org.jetbrains.anko.toast +import kotlin.coroutines.CoroutineContext -open class BaseViewModel(application: Application) : AndroidViewModel(application), CoroutineScope by MainScope(), +open class BaseViewModel(application: Application) : AndroidViewModel(application), + CoroutineScope by MainScope(), AnkoLogger { val context: Context by lazy { this.getApplication() } - fun execute(scope: CoroutineScope = this, block: suspend CoroutineScope.() -> T): Coroutine { - return Coroutine.async(scope) { block() } + fun execute( + scope: CoroutineScope = this, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> T + ): Coroutine { + return Coroutine.async(scope, context) { block() } } - fun submit(scope: CoroutineScope = this, block: suspend CoroutineScope.() -> Deferred): Coroutine { - return Coroutine.async(scope) { block().await() } + fun submit( + scope: CoroutineScope = this, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> Deferred + ): Coroutine { + return Coroutine.async(scope, context) { block().await() } } + @CallSuper override fun onCleared() { super.onCleared() cancel() diff --git a/app/src/main/java/io/legado/app/constant/AppConst.kt b/app/src/main/java/io/legado/app/constant/AppConst.kt index f9fe5b942..99dcb17cb 100644 --- a/app/src/main/java/io/legado/app/constant/AppConst.kt +++ b/app/src/main/java/io/legado/app/constant/AppConst.kt @@ -38,11 +38,7 @@ object AppConst { ) } - val defaultBookGroups by lazy { - listOf( - BookGroup(-1, "全部"), - BookGroup(-2, "本地"), - BookGroup(-3, "音频") - ) - } + val bookGroupAll = BookGroup(-1, App.INSTANCE.getString(R.string.all)) + val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local)) + val bookGroupAudio = BookGroup(-3, App.INSTANCE.getString(R.string.audio)) } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/RSSKeywords.kt b/app/src/main/java/io/legado/app/constant/RSSKeywords.kt index 5a4119e05..bb8d4207a 100644 --- a/app/src/main/java/io/legado/app/constant/RSSKeywords.kt +++ b/app/src/main/java/io/legado/app/constant/RSSKeywords.kt @@ -5,7 +5,7 @@ object RSSKeywords { const val RSS_ITEM = "item" const val RSS_ITEM_TITLE = "title" const val RSS_ITEM_LINK = "link" - const val RSS_ITEM_AUTHOR = "dc:creator" + const val RSS_ITEM_AUTHOR = "author" const val RSS_ITEM_CATEGORY = "category" const val RSS_ITEM_THUMBNAIL = "media:thumbnail" const val RSS_ITEM_ENCLOSURE = "enclosure" diff --git a/app/src/main/java/io/legado/app/data/AppDatabase.kt b/app/src/main/java/io/legado/app/data/AppDatabase.kt index d58a5ccb7..3b17435d5 100644 --- a/app/src/main/java/io/legado/app/data/AppDatabase.kt +++ b/app/src/main/java/io/legado/app/data/AppDatabase.kt @@ -12,7 +12,7 @@ import io.legado.app.data.entities.* @Database( entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, ReplaceRule::class, SearchBook::class, - SearchKeyword::class, SourceCookie::class, RssSource::class, Bookmark::class], + SearchKeyword::class, SourceCookie::class, RssSource::class, Bookmark::class, RssArticle::class], version = 1, exportSchema = true ) @@ -58,4 +58,5 @@ abstract class AppDatabase : RoomDatabase() { abstract fun sourceCookieDao(): SourceCookieDao abstract fun rssSourceDao(): RssSourceDao abstract fun bookmarkDao(): BookmarkDao + abstract fun rssArtivleDao(): RssArticleDao } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/BookDao.kt b/app/src/main/java/io/legado/app/data/dao/BookDao.kt index bae53c8f2..c74172c37 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookDao.kt @@ -24,6 +24,9 @@ interface BookDao { @Query("SELECT bookUrl FROM books WHERE `group` = :group") fun observeUrlsByGroup(group: Int): LiveData> + @Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'") + fun liveDataSearch(key: String): LiveData> + @Query("SELECT * FROM books WHERE `name` in (:names)") fun findByName(vararg names: String): List diff --git a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt index 51068b149..6162ffc5a 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt @@ -1,10 +1,7 @@ package io.legado.app.data.dao import androidx.lifecycle.LiveData -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query +import androidx.room.* import io.legado.app.data.entities.BookGroup @Dao @@ -17,5 +14,11 @@ interface BookGroupDao { val maxId: Int @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(bookGroup: BookGroup) + fun insert(vararg bookGroup: BookGroup) + + @Update + fun update(vararg bookGroup: BookGroup) + + @Delete + fun delete(vararg bookGroup: BookGroup) } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt index 39dc91264..e1864eb5b 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt @@ -11,7 +11,7 @@ interface BookSourceDao { @Query("select * from book_sources order by customOrder asc") fun liveDataAll(): LiveData> - @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> @Query("select * from book_sources where enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' order by customOrder asc") @@ -23,7 +23,7 @@ interface BookSourceDao { @Query("select bookSourceGroup from book_sources where bookSourceGroup is not null and bookSourceGroup <> ''") fun liveGroup(): LiveData> - @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 @Query("update book_sources set enabled = 1 where bookSourceUrl in (:sourceUrls)") @@ -65,6 +65,12 @@ interface BookSourceDao { @Delete fun delete(vararg bookSource: BookSource) + @Query("delete from book_sources where bookSourceUrl = :key") + fun delete(key: String) + @get:Query("select min(customOrder) from book_sources") val minOrder: Int + + @get:Query("select max(customOrder) from book_sources") + val maxOrder: Int } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt b/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt new file mode 100644 index 000000000..0c6290965 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt @@ -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> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(vararg rssArticle: RssArticle) + + @Update + fun update(vararg rssArticle: RssArticle) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt b/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt index 999898598..f280c0fb9 100644 --- a/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt @@ -7,21 +7,48 @@ import io.legado.app.data.entities.RssSource @Dao interface RssSourceDao { + @Query("select * from rssSources where sourceUrl = :key") + fun getByKey(key: String): RssSource? + @get:Query("SELECT * FROM rssSources") val all: List - @Query("SELECT * FROM rssSources") + @Query("SELECT * FROM rssSources order by customOrder") fun liveAll(): LiveData> - @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> + + @Query("SELECT * FROM rssSources where enabled = 1 order by customOrder") fun liveEnabled(): LiveData> + @Query("select sourceGroup from rssSources where sourceGroup is not null and sourceGroup <> ''") + fun liveGroup(): LiveData> + + @Query("update rssSources set enabled = 1 where sourceUrl in (:sourceUrls)") + fun enableSection(vararg sourceUrls: String) + + @Query("update rssSources set enabled = 0 where sourceUrl in (:sourceUrls)") + fun disableSection(vararg sourceUrls: String) + + @get:Query("select min(customOrder) from rssSources") + val minOrder: Int + + @get:Query("select max(customOrder) from rssSources") + val maxOrder: Int + + @Query("delete from rssSources where sourceUrl in (:sourceUrls)") + fun delSection(vararg sourceUrls: String) + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg rssSource: RssSource) @Update fun update(vararg rssSource: RssSource) + @Delete + fun delete(vararg rssSource: RssSource) + @Query("delete from rssSources where sourceUrl = :sourceUrl") fun delete(sourceUrl: String) } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt b/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt index b2370ceb6..be7ad75c5 100644 --- a/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt @@ -1,6 +1,6 @@ package io.legado.app.data.dao -import androidx.paging.DataSource +import androidx.lifecycle.LiveData import androidx.room.* import io.legado.app.data.entities.SearchKeyword @@ -9,16 +9,19 @@ import io.legado.app.data.entities.SearchKeyword interface SearchKeywordDao { @Query("SELECT * FROM search_keywords ORDER BY usage DESC") - fun observeByUsage(): DataSource.Factory + fun liveDataByUsage(): LiveData> @Query("SELECT * FROM search_keywords ORDER BY lastUseTime DESC") - fun observeByTime(): DataSource.Factory + fun liveDataByTime(): LiveData> - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(vararg keywords: SearchKeyword) + @Query("SELECT * FROM search_keywords where word like '%'||:key||'%' ORDER BY usage DESC") + fun liveDataSearch(key: String): LiveData> + + @Query("select * from search_keywords where word = :key") + fun get(key: String): SearchKeyword? @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(keyword: SearchKeyword): Long + fun insert(vararg keywords: SearchKeyword) @Update fun update(vararg keywords: SearchKeyword) diff --git a/app/src/main/java/io/legado/app/data/entities/EditEntity.kt b/app/src/main/java/io/legado/app/data/entities/EditEntity.kt new file mode 100644 index 000000000..d4c1ecc1a --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/EditEntity.kt @@ -0,0 +1,3 @@ +package io.legado.app.data.entities + +data class EditEntity(var key: String, var value: String?, var hint: Int) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/RssArticle.kt b/app/src/main/java/io/legado/app/data/entities/RssArticle.kt index 6f8773ba3..f2deefb1e 100644 --- a/app/src/main/java/io/legado/app/data/entities/RssArticle.kt +++ b/app/src/main/java/io/legado/app/data/entities/RssArticle.kt @@ -1,14 +1,16 @@ package io.legado.app.data.entities import androidx.room.Entity +import androidx.room.Ignore import androidx.room.PrimaryKey @Entity(tableName = "rssArticles") data class RssArticle( var origin: String = "", + var time: Long = System.currentTimeMillis(), @PrimaryKey - var guid: String? = null, + var guid: String = "", var title: String? = null, var author: String? = null, var link: String? = null, @@ -16,5 +18,12 @@ data class RssArticle( var description: String? = null, var content: String? = null, var image: String? = null, - var categories: MutableList = mutableListOf() -) \ No newline at end of file + var categories: String? = null, + var read: Boolean = false, + var star: Boolean = false +) { + + @Ignore + var categoryList: MutableList = mutableListOf() + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/RssSource.kt b/app/src/main/java/io/legado/app/data/entities/RssSource.kt index 17a7064e9..82f0d4863 100644 --- a/app/src/main/java/io/legado/app/data/entities/RssSource.kt +++ b/app/src/main/java/io/legado/app/data/entities/RssSource.kt @@ -2,25 +2,31 @@ package io.legado.app.data.entities import android.os.Parcelable import androidx.room.Entity +import androidx.room.Index import androidx.room.PrimaryKey import kotlinx.android.parcel.Parcelize @Parcelize -@Entity(tableName = "rssSources") +@Entity(tableName = "rssSources", indices = [(Index(value = ["sourceUrl"], unique = false))]) data class RssSource( - var sourceName: String, @PrimaryKey - var sourceUrl: String, - var iconUrl: String, + var sourceUrl: String = "", + var sourceName: String = "", + var sourceIcon: String = "", + var sourceGroup: String? = null, var enabled: Boolean = true, + //列表规则 + var ruleArticles: String? = null, var ruleGuid: String? = null, var ruleTitle: String? = null, var ruleAuthor: String? = null, - var ruleLink: String? = null, var rulePubDate: String? = null, + //类别 + var ruleCategories: String? = null, + //描述 var ruleDescription: String? = null, - var ruleContent: String? = null, var ruleImage: String? = null, - var ruleCategories: String? = null, + var ruleContent: String? = null, + var ruleLink: String? = null, var customOrder: Int = 0 ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt b/app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt index 1c1c5015b..89d1c3baf 100644 --- a/app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt +++ b/app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt @@ -13,5 +13,5 @@ data class SearchKeyword( @PrimaryKey var word: String = "", // 搜索关键词 var usage: Int = 1, // 使用次数 - var lastUseTime: Long = 0 // 最后一次使用时间 + var lastUseTime: Long = System.currentTimeMillis() // 最后一次使用时间 ) : Parcelable diff --git a/app/src/main/java/io/legado/app/help/ReadBookConfig.kt b/app/src/main/java/io/legado/app/help/ReadBookConfig.kt index 915ea9f5a..9238c1341 100644 --- a/app/src/main/java/io/legado/app/help/ReadBookConfig.kt +++ b/app/src/main/java/io/legado/app/help/ReadBookConfig.kt @@ -1,6 +1,7 @@ package io.legado.app.help import android.graphics.Color +import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import io.legado.app.App @@ -52,7 +53,11 @@ object ReadBookConfig { } fun upBg() { - bg = getConfig().bgDrawable() + val resources = App.INSTANCE.resources + val dm = resources.displayMetrics + val width = dm.widthPixels + val height = dm.heightPixels + bg = getConfig().bgDrawable(width, height) } fun save() { @@ -162,20 +167,30 @@ object ReadBookConfig { else bgType } - fun bgDrawable(): Drawable { + fun bgDrawable(width: Int, height: Int): Drawable { var bgDrawable: Drawable? = null - kotlin.runCatching { - when (bgType()) { - 0 -> bgDrawable = ColorDrawable(Color.parseColor(bgStr())) - 1 -> bgDrawable = - Drawable.createFromStream( - App.INSTANCE.assets.open("bg" + File.separator + bgStr()), - "bg" + val resources = App.INSTANCE.resources + try { + bgDrawable = when (bgType()) { + 0 -> ColorDrawable(Color.parseColor(bgStr())) + 1 -> { + BitmapDrawable( + resources, + BitmapUtil.decodeBitmap( + App.INSTANCE, + "bg" + File.separator + bgStr(), + width, + height + ) ) - else -> runCatching { - bgDrawable = Drawable.createFromPath(bgStr()) } + else -> BitmapDrawable( + resources, + BitmapUtil.decodeBitmap(bgStr(), width, height) + ) } + } catch (e: Exception) { + e.printStackTrace() } return bgDrawable ?: ColorDrawable(App.INSTANCE.getCompatColor(R.color.background)) } diff --git a/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt b/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt index 5b8c3d2c0..e9fd0df86 100644 --- a/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt +++ b/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt @@ -1,18 +1,26 @@ package io.legado.app.help.coroutine -import android.util.Log +import io.legado.app.BuildConfig import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext -class Coroutine(scope: CoroutineScope, block: suspend CoroutineScope.() -> T) { +class Coroutine( + scope: CoroutineScope, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> T +) { companion object { val DEFAULT = MainScope() - fun async(scope: CoroutineScope = DEFAULT, block: suspend CoroutineScope.() -> T): Coroutine { - return Coroutine(scope, block) + fun async( + scope: CoroutineScope = DEFAULT, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> T + ): Coroutine { + return Coroutine(scope, context, block) } } @@ -37,7 +45,7 @@ class Coroutine(scope: CoroutineScope, block: suspend CoroutineScope.() -> T) get() = job.isCompleted init { - this.job = executeInternal(scope, block) + this.job = executeInternal(scope, context, block) } fun timeout(timeMillis: () -> Long): Coroutine { @@ -101,13 +109,20 @@ class Coroutine(scope: CoroutineScope, block: suspend CoroutineScope.() -> T) return job.invokeOnCompletion(handler) } - private fun executeInternal(scope: CoroutineScope, block: suspend CoroutineScope.() -> T): Job { + private fun executeInternal( + scope: CoroutineScope, + context: CoroutineContext, + block: suspend CoroutineScope.() -> T + ): Job { return scope.plus(Dispatchers.Main).launch { try { start?.let { dispatchVoidCallback(this, it) } - val value = executeBlock(scope, timeMillis ?: 0L, block) + val value = executeBlock(scope, context, timeMillis ?: 0L, block) success?.let { dispatchCallback(this, value, it) } } catch (e: Throwable) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } val consume: Boolean = errorReturn?.value?.let { value -> success?.let { dispatchCallback(this, value, it) } true @@ -148,10 +163,11 @@ class Coroutine(scope: CoroutineScope, block: suspend CoroutineScope.() -> T) private suspend inline fun executeBlock( scope: CoroutineScope, + context: CoroutineContext, timeMillis: Long, noinline block: suspend CoroutineScope.() -> T ): T? { - return withContext(scope.coroutineContext.plus(Dispatchers.IO)) { + return withContext(scope.coroutineContext.plus(context)) { if (timeMillis > 0L) withTimeout(timeMillis) { block() } else block() diff --git a/app/src/main/java/io/legado/app/help/storage/Backup.kt b/app/src/main/java/io/legado/app/help/storage/Backup.kt index 92333d1d3..fa3ad14f9 100644 --- a/app/src/main/java/io/legado/app/help/storage/Backup.kt +++ b/app/src/main/java/io/legado/app/help/storage/Backup.kt @@ -35,7 +35,7 @@ object Backup { fun autoBackup() { doAsync { - val path = defaultPath + File.separator + "autoBackup" + val path = defaultPath backupBookshelf(path) backupBookSource(path) backupRssSource(path) @@ -45,27 +45,43 @@ object Backup { } private fun backupBookshelf(path: String) { - val json = GSON.toJson(App.db.bookDao().allBooks) - val file = FileHelp.getFile(path + File.separator + "bookshelf.json") - file.writeText(json) + App.db.bookDao().allBooks.let { + if (it.isNotEmpty()) { + val json = GSON.toJson(it) + val file = FileHelp.getFile(path + File.separator + "bookshelf.json") + file.writeText(json) + } + } } private fun backupBookSource(path: String) { - val json = GSON.toJson(App.db.bookSourceDao().all) - val file = FileHelp.getFile(path + File.separator + "bookSource.json") - file.writeText(json) + App.db.bookSourceDao().all.let { + if (it.isNotEmpty()) { + val json = GSON.toJson(it) + val file = FileHelp.getFile(path + File.separator + "bookSource.json") + file.writeText(json) + } + } } private fun backupRssSource(path: String) { - val json = GSON.toJson(App.db.rssSourceDao().all) - val file = FileHelp.getFile(path + File.separator + "rssSource.json") - file.writeText(json) + App.db.rssSourceDao().all.let { + if (it.isNotEmpty()) { + val json = GSON.toJson(it) + val file = FileHelp.getFile(path + File.separator + "rssSource.json") + file.writeText(json) + } + } } private fun backupReplaceRule(path: String) { - val json = GSON.toJson(App.db.replaceRuleDao().all) - val file = FileHelp.getFile(path + File.separator + "replaceRule.json") - file.writeText(json) + App.db.replaceRuleDao().all.let { + if (it.isNotEmpty()) { + val json = GSON.toJson(it) + val file = FileHelp.getFile(path + File.separator + "replaceRule.json") + file.writeText(json) + } + } } private fun backupPreference(path: String) { diff --git a/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt b/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt index 128fb6078..3d82b5320 100644 --- a/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt +++ b/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt @@ -39,8 +39,8 @@ object WebDavHelp { val url = getWebDavUrl() val names = arrayListOf() if (!url.isNullOrBlank() && initWebDav()) { - val files = WebDav(url + "legado/").listFiles() - files.reversed() + var files = WebDav(url + "legado/").listFiles() + files = files.reversed() for (index: Int in 0 until min(10, files.size)) { files[index].displayName?.let { names.add(it) diff --git a/app/src/main/java/io/legado/app/model/Rss.kt b/app/src/main/java/io/legado/app/model/Rss.kt new file mode 100644 index 000000000..cd76e24fe --- /dev/null +++ b/app/src/main/java/io/legado/app/model/Rss.kt @@ -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> { + return Coroutine.async(scope, context) { + val response = AnalyzeUrl(rssSource.sourceUrl).getResponseAsync().await() + RssParserByRule.parseXML(response, rssSource) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/WebBook.kt b/app/src/main/java/io/legado/app/model/WebBook.kt index a09e9f326..9201fd542 100644 --- a/app/src/main/java/io/legado/app/model/WebBook.kt +++ b/app/src/main/java/io/legado/app/model/WebBook.kt @@ -11,6 +11,8 @@ import io.legado.app.model.webbook.BookContent import io.legado.app.model.webbook.BookInfo import io.legado.app.model.webbook.BookList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlin.coroutines.CoroutineContext class WebBook(val bookSource: BookSource) { @@ -20,9 +22,13 @@ class WebBook(val bookSource: BookSource) { /** * 搜索 */ - fun searchBook(key: String, page: Int? = 1, scope: CoroutineScope = Coroutine.DEFAULT) - : Coroutine> { - return Coroutine.async(scope) { + fun searchBook( + key: String, + page: Int? = 1, + scope: CoroutineScope = Coroutine.DEFAULT, + context: CoroutineContext = Dispatchers.IO + ): Coroutine> { + return Coroutine.async(scope, context) { bookSource.searchUrl?.let { searchUrl -> val analyzeUrl = AnalyzeUrl( ruleUrl = searchUrl, @@ -40,9 +46,13 @@ class WebBook(val bookSource: BookSource) { /** * 发现 */ - fun exploreBook(url: String, page: Int? = 1, scope: CoroutineScope = Coroutine.DEFAULT) - : Coroutine> { - return Coroutine.async(scope) { + fun exploreBook( + url: String, + page: Int? = 1, + scope: CoroutineScope = Coroutine.DEFAULT, + context: CoroutineContext = Dispatchers.IO + ): Coroutine> { + return Coroutine.async(scope, context) { val analyzeUrl = AnalyzeUrl( ruleUrl = url, page = page, @@ -57,8 +67,12 @@ class WebBook(val bookSource: BookSource) { /** * 书籍信息 */ - fun getBookInfo(book: Book, scope: CoroutineScope = Coroutine.DEFAULT): Coroutine { - return Coroutine.async(scope) { + fun getBookInfo( + book: Book, + scope: CoroutineScope = Coroutine.DEFAULT, + context: CoroutineContext = Dispatchers.IO + ): Coroutine { + return Coroutine.async(scope, context) { val analyzeUrl = AnalyzeUrl( book = book, ruleUrl = book.bookUrl, @@ -76,9 +90,10 @@ class WebBook(val bookSource: BookSource) { */ fun getChapterList( book: Book, - scope: CoroutineScope = Coroutine.DEFAULT + scope: CoroutineScope = Coroutine.DEFAULT, + context: CoroutineContext = Dispatchers.IO ): Coroutine> { - return Coroutine.async(scope) { + return Coroutine.async(scope, context) { val analyzeUrl = AnalyzeUrl( book = book, ruleUrl = book.tocUrl, @@ -97,9 +112,10 @@ class WebBook(val bookSource: BookSource) { book: Book, bookChapter: BookChapter, nextChapterUrl: String? = null, - scope: CoroutineScope = Coroutine.DEFAULT + scope: CoroutineScope = Coroutine.DEFAULT, + context: CoroutineContext = Dispatchers.IO ): Coroutine { - return Coroutine.async(scope) { + return Coroutine.async(scope, context) { val analyzeUrl = AnalyzeUrl( book = book, diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt index bc38b8571..c04981cdd 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt @@ -445,7 +445,7 @@ class AnalyzeRule(private var book: BaseBook? = null) { } //分离正则表达式 val ruleStrS = - rule.trim { it <= ' ' }.splitNotBlank("##") + rule.trim { it <= ' ' }.split("##") rule = ruleStrS[0] if (ruleStrS.size > 1) { replaceRegex = ruleStrS[1] diff --git a/app/src/main/java/io/legado/app/model/rss/RssParser.kt b/app/src/main/java/io/legado/app/model/rss/RssParser.kt index 2696fb26a..7363a349e 100644 --- a/app/src/main/java/io/legado/app/model/rss/RssParser.kt +++ b/app/src/main/java/io/legado/app/model/rss/RssParser.kt @@ -11,7 +11,7 @@ import java.io.StringReader object RssParser { @Throws(XmlPullParserException::class, IOException::class) - fun parseXML(xml: String): MutableList { + fun parseXML(xml: String, sourceUrl: String): MutableList { val articleList = mutableListOf() var currentArticle = RssArticle() @@ -42,7 +42,7 @@ object RssParser { xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_AUTHOR, true) -> if (insideItem) currentArticle.author = xmlPullParser.nextText().trim() xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_CATEGORY, true) -> - if (insideItem) currentArticle.categories.add(xmlPullParser.nextText().trim()) + if (insideItem) currentArticle.categoryList.add(xmlPullParser.nextText().trim()) xmlPullParser.name.equals(RSSKeywords.RSS_ITEM_THUMBNAIL, true) -> if (insideItem) currentArticle.image = xmlPullParser.getAttributeValue(null, RSSKeywords.RSS_ITEM_URL) @@ -92,6 +92,8 @@ object RssParser { ) { // The item is correctly parsed insideItem = false + currentArticle.categories = currentArticle.categoryList.joinToString(",") + currentArticle.origin = sourceUrl articleList.add(currentArticle) currentArticle = RssArticle() } diff --git a/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt b/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt new file mode 100644 index 000000000..45bbaf637 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt @@ -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, rssSource: RssSource): MutableList { + val articleList = mutableListOf() + + 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, + ruleTitle: List, + ruleAuthor: List, + rulePubDate: List, + ruleCategories: List, + ruleDescription: List, + ruleImage: List, + ruleContent: List, + ruleLink: List + ): 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt b/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt index 17eba6b18..ce3908021 100644 --- a/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt +++ b/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt @@ -7,6 +7,7 @@ import android.view.KeyEvent import io.legado.app.constant.Bus import io.legado.app.help.ActivityHelp import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.utils.LogUtils import io.legado.app.utils.postEvent @@ -22,7 +23,7 @@ class MediaButtonReceiver : BroadcastReceiver() { fun handleIntent(context: Context, intent: Intent): Boolean { val intentAction = intent.action val keyEventAction = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)?.action -// LogUtils.log("耳机按键: $intentAction $keyEventAction") + LogUtils.d("耳机按键", "$intentAction $keyEventAction") if (Intent.ACTION_MEDIA_BUTTON == intentAction) { if (keyEventAction == KeyEvent.ACTION_DOWN) { readAloud(context) diff --git a/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt b/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt index e98523051..093c5a9ec 100644 --- a/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt +++ b/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt @@ -24,7 +24,7 @@ class AboutFragment : PreferenceFragmentCompat() { override fun onPreferenceTreeClick(preference: Preference?): Boolean { when (preference?.key) { "mail" -> openIntent(Intent.ACTION_SENDTO, "mailto:kunfei.ge@gmail.com") - "gitHub" -> openIntent(Intent.ACTION_VIEW, getString(R.string.this_github_url)) + "git" -> openIntent(Intent.ACTION_VIEW, getString(R.string.this_github_url)) } return super.onPreferenceTreeClick(preference) } diff --git a/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt b/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt index 313a6bec6..6d0671029 100644 --- a/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt +++ b/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt @@ -8,11 +8,10 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.widget.Toast -import androidx.lifecycle.AndroidViewModel import io.legado.app.R import io.legado.app.base.BaseActivity import io.legado.app.lib.theme.ATH -import io.legado.app.utils.getViewModel +import io.legado.app.utils.ACache import kotlinx.android.synthetic.main.activity_donate.* import kotlinx.android.synthetic.main.view_title_bar.* import org.jetbrains.anko.toast @@ -60,7 +59,7 @@ class DonateActivity : BaseActivity(R.layout.activity_donate) { } catch (e: Exception) { e.printStackTrace() } finally { - + ACache.get(this, cacheDir = false).put("proTime", System.currentTimeMillis()) } } diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt index 62bc33ca9..b9bb67838 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt @@ -14,7 +14,7 @@ import io.legado.app.help.ImageLoader import io.legado.app.lib.theme.ATH import io.legado.app.ui.book.info.edit.BookInfoEditActivity import io.legado.app.ui.book.read.ReadBookActivity -import io.legado.app.ui.book.source.edit.SourceEditActivity +import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.changesource.ChangeSourceDialog import io.legado.app.utils.getCompatDrawable import io.legado.app.utils.getViewModel @@ -88,8 +88,8 @@ class BookInfoActivity : VMBaseActivity(R.layout.activity_boo book.getDisplayIntro() // getString(R.string.intro_show, book.getDisplayIntro()) book.getDisplayCover()?.let { ImageLoader.load(this, it) - .placeholder(R.drawable.img_cover_default) - .error(R.drawable.img_cover_default) + .placeholder(R.drawable.image_cover_default) + .error(R.drawable.image_cover_default) .centerCrop() .setAsDrawable(iv_cover) } @@ -183,10 +183,14 @@ class BookInfoActivity : VMBaseActivity(R.layout.activity_boo } } } - tv_loading.onClick { } + tv_loading.onClick { + viewModel.bookData.value?.let { + viewModel.loadBookInfo(it) + } + } tv_origin.onClick { viewModel.bookData.value?.let { - startActivity(Pair("data", it.origin)) + startActivity(Pair("data", it.origin)) } } tv_change_source.onClick { @@ -204,8 +208,10 @@ class BookInfoActivity : VMBaseActivity(R.layout.activity_boo } } iv_chapter_top.onClick { - adapter.reorder = !adapter.reorder - adapter.notifyDataSetChanged() + rv_chapter_list.scrollToPosition(0) + } + iv_chapter_bottom.onClick { + rv_chapter_list.scrollToPosition(adapter.itemCount - 1) } } @@ -226,13 +232,11 @@ class BookInfoActivity : VMBaseActivity(R.layout.activity_boo } } - override fun curOrigin(): String? { - return viewModel.bookData.value?.origin - } + override val curOrigin: String? + get() = viewModel.bookData.value?.origin - override fun oldBook(): Book? { - return viewModel.bookData.value - } + override val oldBook: Book? + get() = viewModel.bookData.value override fun changeTo(book: Book) { upLoading(true) diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt index cb54f4df1..dc96b4c0c 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt @@ -99,7 +99,6 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { } }.onError { toast(R.string.error_get_chapter_list) - it.printStackTrace() } } ?: toast(R.string.error_no_source) } @@ -130,7 +129,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { chapters ) book.durChapterTitle = chapters[book.durChapterIndex].title - App.db.bookDao().update(book) + App.db.bookDao().insert(book) App.db.bookChapterDao().insert(*chapters.toTypedArray()) bookData.postValue(book) chapterListData.postValue(chapters) diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index dbcc05184..c763ae746 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -32,16 +32,17 @@ import io.legado.app.service.BaseReadAloudService import io.legado.app.ui.book.read.config.* import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR -import io.legado.app.ui.book.source.edit.SourceEditActivity +import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.changesource.ChangeSourceDialog import io.legado.app.ui.chapterlist.ChapterListActivity import io.legado.app.ui.replacerule.ReplaceRuleActivity +import io.legado.app.ui.replacerule.edit.ReplaceEditDialog import io.legado.app.ui.widget.page.ChapterProvider import io.legado.app.ui.widget.page.PageView import io.legado.app.ui.widget.page.TextChapter import io.legado.app.ui.widget.page.delegate.PageDelegate import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_read_book.* +import kotlinx.android.synthetic.main.activity_book_read.* import kotlinx.android.synthetic.main.view_book_page.* import kotlinx.android.synthetic.main.view_read_menu.* import kotlinx.android.synthetic.main.view_title_bar.* @@ -54,7 +55,7 @@ import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivityForResult import org.jetbrains.anko.toast -class ReadBookActivity : VMBaseActivity(R.layout.activity_read_book), +class ReadBookActivity : VMBaseActivity(R.layout.activity_book_read), PageView.CallBack, ReadMenu.CallBack, ReadAloudDialog.CallBack, @@ -113,7 +114,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_rea private fun initView() { tv_chapter_name.onClick { viewModel.webBook?.let { - startActivityForResult( + startActivityForResult( requestCodeEditSource, Pair("data", it.bookSource.bookSourceUrl) ) @@ -304,7 +305,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_rea tv_chapter_url.text = it.url tv_chapter_url.visible() } - read_menu.upReadProgress(it.pageSize().minus(1), viewModel.durPageIndex) + seek_read_page.max = it.pageSize().minus(1) tv_pre.isEnabled = viewModel.durChapterIndex != 0 tv_next.isEnabled = viewModel.durChapterIndex != viewModel.chapterSize - 1 curPageChanged() @@ -312,6 +313,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_rea } private fun curPageChanged() { + seek_read_page.progress = viewModel.durPageIndex when (readAloudStatus) { Status.PLAY -> readAloud() Status.PAUSE -> { @@ -328,13 +330,11 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_rea return viewModel.chapterSize } - override fun curOrigin(): String? { - return viewModel.bookData.value?.origin - } + override val curOrigin: String? + get() = viewModel.bookData.value?.origin - override fun oldBook(): Book? { - return viewModel.bookData.value - } + override val oldBook: Book? + get() = viewModel.bookData.value override fun changeTo(book: Book) { viewModel.changeTo(book) @@ -608,7 +608,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_rea } } observeEvent(Bus.REPLACE) { - toast(it) + ReplaceEditDialog().show(supportFragmentManager, "replaceEditDialog") } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt index a0b02a402..d6d6ae004 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -68,7 +68,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { } } saveRead(book) - }.onError { it.printStackTrace() } + } } private fun loadBookInfo( @@ -103,7 +103,6 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { } }?.onError { toast(R.string.error_load_toc) - it.printStackTrace() } ?: autoChangeSource() } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt index bb321fd65..0aa08497c 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt @@ -95,11 +95,6 @@ class ReadMenu : FrameLayout { } } - fun upReadProgress(max: Int, dur: Int) { - seek_read_page.max = max - seek_read_page.progress = dur - } - private fun brightnessAuto(): Boolean { return context.getPrefBoolean("brightnessAuto", true) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt index ea80392c8..d64fc4db9 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt @@ -20,7 +20,7 @@ import io.legado.app.ui.book.read.Help import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.widget.font.FontSelectDialog import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_read_book.* +import kotlinx.android.synthetic.main.activity_book_read.* import kotlinx.android.synthetic.main.dialog_read_book_style.* import org.jetbrains.anko.sdk27.listeners.onCheckedChange import org.jetbrains.anko.sdk27.listeners.onClick @@ -249,7 +249,7 @@ class ReadStyleDialog : DialogFragment() { ReadBookConfig.getConfig(i).apply { when (bgType()) { 2 -> ImageLoader.load(requireContext(), bgStr()).centerCrop().setAsDrawable(iv) - else -> iv.setImageDrawable(bgDrawable()) + else -> iv.setImageDrawable(bgDrawable(100, 150)) } } } diff --git a/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt b/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt new file mode 100644 index 000000000..3a08418e1 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt @@ -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(context, R.layout.item_text) { + + override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList) { + with(holder.itemView) { + text_view.text = item.name + onClick { callBack.showBookInfo(item.bookUrl) } + } + } + + interface CallBack { + fun showBookInfo(url: String) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt b/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt new file mode 100644 index 000000000..213157e90 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt @@ -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(context, R.layout.item_text) { + + override fun convert(holder: ItemViewHolder, item: SearchKeyword, payloads: MutableList) { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt index 64d5d76f8..bc25fa64f 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt @@ -7,32 +7,44 @@ import androidx.lifecycle.Observer import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.SearchKeyword import io.legado.app.data.entities.SearchShow import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.primaryTextColor import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.utils.getViewModel +import io.legado.app.utils.gone import io.legado.app.utils.invisible import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.activity_search.* +import kotlinx.android.synthetic.main.activity_book_search.* import kotlinx.android.synthetic.main.view_search.* import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.startActivity -class SearchActivity : VMBaseActivity(R.layout.activity_search), SearchAdapter.CallBack { +class SearchActivity : VMBaseActivity(R.layout.activity_book_search), + BookAdapter.CallBack, + HistoryKeyAdapter.CallBack, + SearchAdapter.CallBack { override val viewModel: SearchViewModel get() = getViewModel(SearchViewModel::class.java) private lateinit var adapter: SearchAdapter + private lateinit var bookAdapter: BookAdapter + private lateinit var historyKeyAdapter: HistoryKeyAdapter private var searchBookData: LiveData>? = null + private var historyData: LiveData>? = null + private var bookData: LiveData>? = null override fun onActivityCreated(savedInstanceState: Bundle?) { initRecyclerView() initSearchView() + initOtherView() initData() intent.getStringExtra("key")?.let { search_view.setQuery(it, true) @@ -49,6 +61,7 @@ class SearchActivity : VMBaseActivity(R.layout.activity_search) override fun onQueryTextSubmit(query: String?): Boolean { search_view.clearFocus() query?.let { + viewModel.saveSearchKey(query) viewModel.search(it, { refresh_progress_bar.isAutoLoading = true initData() @@ -63,19 +76,43 @@ class SearchActivity : VMBaseActivity(R.layout.activity_search) override fun onQueryTextChange(newText: String?): Boolean { if (newText.isNullOrBlank()) viewModel.stop() + upHistory(newText) return false } }) - fb_stop.onClick { viewModel.stop() } + search_view.setOnQueryTextFocusChangeListener { _, hasFocus -> + if (hasFocus) { + ll_history.visible() + } else { + ll_history.invisible() + } + } } private fun initRecyclerView() { ATH.applyEdgeEffectColor(recycler_view) + ATH.applyEdgeEffectColor(rv_bookshelf_search) + ATH.applyEdgeEffectColor(rv_history_key) + bookAdapter = BookAdapter(this, this) + rv_bookshelf_search.layoutManager = + LinearLayoutManager(this, RecyclerView.HORIZONTAL, false) + rv_bookshelf_search.adapter = bookAdapter + historyKeyAdapter = HistoryKeyAdapter(this, this) + rv_history_key.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false) + rv_history_key.adapter = historyKeyAdapter adapter = SearchAdapter(this) recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.adapter = adapter } + private fun initOtherView() { + tv_clear_history.onClick { viewModel.clearHistory() } + fb_stop.onClick { + viewModel.stop() + refresh_progress_bar.isAutoLoading = false + } + } + private fun initData() { searchBookData?.removeObservers(this) searchBookData = LivePagedListBuilder( @@ -85,6 +122,35 @@ class SearchActivity : VMBaseActivity(R.layout.activity_search) ), 30 ).build() searchBookData?.observe(this, Observer { adapter.submitList(it) }) + upHistory() + } + + private fun upHistory(key: String? = null) { + bookData?.removeObservers(this) + if (key.isNullOrBlank()) { + tv_book_show.gone() + rv_bookshelf_search.gone() + } else { + bookData = App.db.bookDao().liveDataSearch(key) + bookData?.observe(this, Observer { + if (it.isEmpty()) { + tv_book_show.gone() + rv_bookshelf_search.gone() + } else { + tv_book_show.visible() + rv_bookshelf_search.visible() + } + bookAdapter.setItems(it) + }) + } + historyData?.removeObservers(this) + historyData = + if (key.isNullOrBlank()) { + App.db.searchKeywordDao().liveDataByUsage() + } else { + App.db.searchKeywordDao().liveDataSearch(key) + } + historyData?.observe(this, Observer { historyKeyAdapter.setItems(it) }) } override fun showBookInfo(name: String, author: String) { @@ -94,4 +160,12 @@ class SearchActivity : VMBaseActivity(R.layout.activity_search) } } } + + override fun showBookInfo(url: String) { + startActivity(Pair("bookUrl", url)) + } + + override fun searchHistory(key: String) { + search_view.setQuery(key, false) + } } diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt index a2cb9ad08..38974157d 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt @@ -84,8 +84,8 @@ class SearchAdapter(val callBack: CallBack) : } searchBook.coverUrl.let { ImageLoader.load(context, it)//Glide自动识别http://和file:// - .placeholder(R.drawable.img_cover_default) - .error(R.drawable.img_cover_default) + .placeholder(R.drawable.image_cover_default) + .error(R.drawable.image_cover_default) .centerCrop() .setAsDrawable(iv_cover) } @@ -100,8 +100,8 @@ class SearchAdapter(val callBack: CallBack) : 1 -> bv_originCount.setBadgeCount(searchBook.originCount) 2 -> searchBook.coverUrl.let { ImageLoader.load(context, it)//Glide自动识别http://和file:// - .placeholder(R.drawable.img_cover_default) - .error(R.drawable.img_cover_default) + .placeholder(R.drawable.image_cover_default) + .error(R.drawable.image_cover_default) .centerCrop() .setAsDrawable(iv_cover) } diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt index f8550229f..0c21d1820 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt @@ -4,12 +4,15 @@ import android.app.Application import io.legado.app.App import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.SearchBook +import io.legado.app.data.entities.SearchKeyword import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.WebBook import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay +import kotlinx.coroutines.asCoroutineDispatcher +import java.util.concurrent.Executors class SearchViewModel(application: Application) : BaseViewModel(application) { + private var searchPool = Executors.newFixedThreadPool(16).asCoroutineDispatcher() private var task: Coroutine<*>? = null var searchKey: String = "" var startTime: Long = 0 @@ -30,7 +33,12 @@ class SearchViewModel(application: Application) : BaseViewModel(application) { val bookSourceList = App.db.bookSourceDao().allEnabled for (item in bookSourceList) { //task取消时自动取消 by (scope = this@execute) - WebBook(item).searchBook(key, searchPage, scope = this@execute) + WebBook(item).searchBook( + key, + searchPage, + scope = this@execute, + context = searchPool + ) .timeout(30000L) .onSuccess(Dispatchers.IO) { it?.let { list -> @@ -40,10 +48,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) { } } } - delay(100)//每隔100毫秒搜索一个书源 } - }.onError { - it.printStackTrace() } task?.invokeOnCompletion { @@ -61,4 +66,24 @@ class SearchViewModel(application: Application) : BaseViewModel(application) { success?.invoke(searchBook) } } + + fun saveSearchKey(key: String) { + execute { + App.db.searchKeywordDao().get(key)?.let { + it.usage = it.usage + 1 + App.db.searchKeywordDao().update(it) + } ?: App.db.searchKeywordDao().insert(SearchKeyword(key, 1)) + } + } + + fun clearHistory() { + execute { + App.db.searchKeywordDao().deleteAll() + } + } + + override fun onCleared() { + super.onCleared() + searchPool.close() + } } diff --git a/app/src/main/java/io/legado/app/ui/book/source/debug/SourceDebugActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt similarity index 88% rename from app/src/main/java/io/legado/app/ui/book/source/debug/SourceDebugActivity.kt rename to app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt index b180f6b17..dafeaa218 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/debug/SourceDebugActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt @@ -12,18 +12,19 @@ import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.accentColor import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.utils.getViewModel -import kotlinx.android.synthetic.main.activity_book_source_debug.* +import kotlinx.android.synthetic.main.activity_source_debug.* import kotlinx.android.synthetic.main.view_search.* import kotlinx.coroutines.launch import org.jetbrains.anko.startActivityForResult import org.jetbrains.anko.toast -class SourceDebugActivity : VMBaseActivity(R.layout.activity_book_source_debug) { +class BookSourceDebugActivity : + VMBaseActivity(R.layout.activity_source_debug) { - override val viewModel: SourceDebugModel - get() = getViewModel(SourceDebugModel::class.java) + override val viewModel: BookSourceDebugModel + get() = getViewModel(BookSourceDebugModel::class.java) - private lateinit var adapter: SourceDebugAdapter + private lateinit var adapter: BookSourceDebugAdapter private val qrRequestCode = 101 override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -42,7 +43,7 @@ class SourceDebugActivity : VMBaseActivity(R.layout.activity_b private fun initRecyclerView() { ATH.applyEdgeEffectColor(recycler_view) - adapter = SourceDebugAdapter(this) + adapter = BookSourceDebugAdapter(this) recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.adapter = adapter rotate_loading.loadingColor = accentColor diff --git a/app/src/main/java/io/legado/app/ui/book/source/debug/SourceDebugAdapter.kt b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt similarity index 90% rename from app/src/main/java/io/legado/app/ui/book/source/debug/SourceDebugAdapter.kt rename to app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt index 94147a5f1..2ec4e9518 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/debug/SourceDebugAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt @@ -6,7 +6,7 @@ import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter import kotlinx.android.synthetic.main.item_log.view.* -class SourceDebugAdapter(context: Context) : +class BookSourceDebugAdapter(context: Context) : SimpleRecyclerAdapter(context, R.layout.item_log) { override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { holder.itemView.apply { diff --git a/app/src/main/java/io/legado/app/ui/book/source/debug/SourceDebugModel.kt b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt similarity index 91% rename from app/src/main/java/io/legado/app/ui/book/source/debug/SourceDebugModel.kt rename to app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt index a17565820..ced985aa9 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/debug/SourceDebugModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt @@ -6,7 +6,8 @@ import io.legado.app.base.BaseViewModel import io.legado.app.model.WebBook import io.legado.app.model.webbook.SourceDebug -class SourceDebugModel(application: Application) : BaseViewModel(application), SourceDebug.Callback { +class BookSourceDebugModel(application: Application) : BaseViewModel(application), + SourceDebug.Callback { private var webBook: WebBook? = null diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt new file mode 100644 index 000000000..267890628 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt @@ -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(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 = ArrayList() + private val searchEntities: ArrayList = ArrayList() + private val findEntities: ArrayList = ArrayList() + private val infoEntities: ArrayList = ArrayList() + private val tocEntities: ArrayList = ArrayList() + private val contentEntities: ArrayList = 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(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() + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/SourceEditAdapter.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt similarity index 90% rename from app/src/main/java/io/legado/app/ui/book/source/edit/SourceEditAdapter.kt rename to app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt index 3ac5fc3d1..37939e803 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/edit/SourceEditAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt @@ -7,11 +7,12 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import io.legado.app.R +import io.legado.app.data.entities.EditEntity import kotlinx.android.synthetic.main.item_source_edit.view.* -class SourceEditAdapter : RecyclerView.Adapter() { +class BookSourceEditAdapter : RecyclerView.Adapter() { - var editEntities: ArrayList = ArrayList() + var editEntities: ArrayList = ArrayList() set(value) { field = value notifyDataSetChanged() @@ -34,7 +35,7 @@ class SourceEditAdapter : RecyclerView.Adapter() } class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) { - fun bind(editEntity: SourceEditActivity.EditEntity) = with(itemView) { + fun bind(editEntity: EditEntity) = with(itemView) { if (editText.getTag(R.id.tag1) == null) { val listener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/SourceEditViewModel.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt similarity index 58% rename from app/src/main/java/io/legado/app/ui/book/source/edit/SourceEditViewModel.kt rename to app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt index c3af29a6a..61cc49858 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/edit/SourceEditViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt @@ -3,29 +3,44 @@ package io.legado.app.ui.book.source.edit import android.app.Application import android.content.ClipboardManager import android.content.Context +import android.content.Intent import androidx.lifecycle.MutableLiveData import io.legado.app.App import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.BookSource import io.legado.app.help.storage.OldRule -class SourceEditViewModel(application: Application) : BaseViewModel(application) { +class BookSourceEditViewModel(application: Application) : BaseViewModel(application) { val sourceLiveData: MutableLiveData = MutableLiveData() + var oldSourceUrl: String? = null - fun setBookSource(key: String) { + fun initData(intent: Intent) { execute { - App.db.bookSourceDao().getBookSource(key)?.let { + val key = intent.getStringExtra("data") + var source: BookSource? = null + if (key != null) { + source = App.db.bookSourceDao().getBookSource(key) + } + source?.let { + oldSourceUrl = it.bookSourceUrl sourceLiveData.postValue(it) - } ?: sourceLiveData.postValue(BookSource()) + } ?: let { + sourceLiveData.postValue(BookSource().apply { + customOrder = App.db.bookSourceDao().maxOrder + 1 + }) + } } } fun save(bookSource: BookSource, finally: (() -> Unit)? = null) { execute { - if (bookSource.customOrder == 0) { - bookSource.customOrder = App.db.bookSourceDao().allCount() + oldSourceUrl?.let { + if (oldSourceUrl != bookSource.bookSourceUrl) { + App.db.bookSourceDao().delete(it) + } } + oldSourceUrl = bookSource.bookSourceUrl App.db.bookSourceDao().insert(bookSource) }.onFinally { finally?.let { it() } diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/SourceEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/SourceEditActivity.kt deleted file mode 100644 index 8a2db83d6..000000000 --- a/app/src/main/java/io/legado/app/ui/book/source/edit/SourceEditActivity.kt +++ /dev/null @@ -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(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 = ArrayList() - private val searchEntities: ArrayList = ArrayList() - private val findEntities: ArrayList = ArrayList() - private val infoEntities: ArrayList = ArrayList() - private val tocEntities: ArrayList = ArrayList() - private val contentEntities: ArrayList = 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("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) -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt index bf357319f..bc9da05fb 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt @@ -28,7 +28,7 @@ import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.primaryTextColor import io.legado.app.lib.theme.view.ATEAutoCompleteTextView import io.legado.app.service.CheckSourceService -import io.legado.app.ui.book.source.edit.SourceEditActivity +import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.utils.ACache import io.legado.app.utils.applyTint @@ -78,7 +78,7 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_add_book_source -> { - this.startActivity() + this.startActivity() } R.id.menu_import_book_source_qr -> { this.startActivityForResult(qrRequestCode) @@ -151,12 +151,8 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity } bookSourceLiveDate?.observe(this, Observer { search_view.queryHint = getString(R.string.search_book_source_num, it.size) - val diffResult = DiffUtil.calculateDiff( - DiffCallBack( - adapter.getItems(), - it - ) - ) + val diffResult = DiffUtil + .calculateDiff(DiffCallBack(adapter.getItems(), it)) adapter.setItemsNoNotify(it) diffResult.dispatchUpdatesTo(adapter) }) @@ -232,7 +228,7 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity } override fun edit(bookSource: BookSource) { - startActivity(Pair("data", bookSource.bookSourceUrl)) + startActivity(Pair("data", bookSource.bookSourceUrl)) } override fun upOrder() { diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt index 31accc240..a51b589b3 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt @@ -16,8 +16,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) fun topSource(bookSource: BookSource) { execute { - val minXh = App.db.bookSourceDao().minOrder - bookSource.customOrder = minXh - 1 + bookSource.customOrder = App.db.bookSourceDao().minOrder - 1 App.db.bookSourceDao().insert(bookSource) } } diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt index 1cdcccd45..59f5e73af 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt @@ -29,7 +29,6 @@ import io.legado.app.utils.requestInputMethod import io.legado.app.utils.splitNotBlank import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_recycler_view.* -import kotlinx.android.synthetic.main.item_book_group.view.tv_group import kotlinx.android.synthetic.main.item_group_manage.view.* import org.jetbrains.anko.sdk27.listeners.onClick diff --git a/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceDialog.kt b/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceDialog.kt index 8f02698b9..674db46c1 100644 --- a/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceDialog.kt +++ b/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceDialog.kt @@ -35,6 +35,7 @@ class ChangeSourceDialog : DialogFragment(), } } + private var callBack: CallBack? = null private lateinit var viewModel: ChangeSourceViewModel private lateinit var changeSourceAdapter: ChangeSourceAdapter @@ -49,6 +50,7 @@ class ChangeSourceDialog : DialogFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + callBack = activity as? CallBack viewModel.searchStateData.observe(viewLifecycleOwner, Observer { refresh_progress_bar.isAutoLoading = it }) @@ -114,31 +116,24 @@ class ChangeSourceDialog : DialogFragment(), } override fun changeTo(searchBook: SearchBook) { - val activity = activity - if (activity is CallBack) { - val book = searchBook.toBook() - activity.oldBook()?.let { oldBook -> - book.durChapterIndex = oldBook.durChapterIndex - book.durChapterPos = oldBook.durChapterPos - book.durChapterTitle = oldBook.durChapterTitle - book.customCoverUrl = oldBook.customCoverUrl - book.customIntro = oldBook.customIntro - book.order = oldBook.order - if (book.coverUrl.isNullOrEmpty()) { - book.coverUrl = oldBook.getDisplayCover() - } - activity.changeTo(book) + val book = searchBook.toBook() + callBack?.oldBook?.let { oldBook -> + book.durChapterIndex = oldBook.durChapterIndex + book.durChapterPos = oldBook.durChapterPos + book.durChapterTitle = oldBook.durChapterTitle + book.customCoverUrl = oldBook.customCoverUrl + book.customIntro = oldBook.customIntro + book.order = oldBook.order + if (book.coverUrl.isNullOrEmpty()) { + book.coverUrl = oldBook.getDisplayCover() } + callBack?.changeTo(book) } dismiss() } override fun curOrigin(): String { - val activity = activity - if (activity is CallBack) { - return activity.curOrigin() ?: "" - } - return "" + return callBack?.curOrigin ?: "" } override fun adapter(): ChangeSourceAdapter { @@ -146,8 +141,8 @@ class ChangeSourceDialog : DialogFragment(), } interface CallBack { - fun curOrigin(): String? - fun oldBook(): Book? + val curOrigin: String? + val oldBook: Book? fun changeTo(book: Book) } diff --git a/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt index db4a5e889..44483aa07 100644 --- a/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt @@ -12,11 +12,13 @@ import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.WebBook import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.delay +import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.withContext import org.jetbrains.anko.debug +import java.util.concurrent.Executors class ChangeSourceViewModel(application: Application) : BaseViewModel(application) { + private var searchPool = Executors.newFixedThreadPool(16).asCoroutineDispatcher() var callBack: CallBack? = null val searchStateData = MutableLiveData() var name: String = "" @@ -56,7 +58,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio val bookSourceList = App.db.bookSourceDao().allEnabled for (item in bookSourceList) { //task取消时自动取消 by (scope = this@execute) - WebBook(item).searchBook(name, scope = this@execute) + WebBook(item).searchBook(name, scope = this@execute, context = searchPool) .timeout(30000L) .onSuccess(IO) { it?.forEach { searchBook -> @@ -70,7 +72,6 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio } } } - delay(100) } } @@ -130,4 +131,9 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio interface CallBack { fun adapter(): ChangeSourceAdapter } + + override fun onCleared() { + super.onCleared() + searchPool.close() + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/ConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/ConfigFragment.kt index c3b0ae799..fd957e433 100644 --- a/app/src/main/java/io/legado/app/ui/config/ConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/ConfigFragment.kt @@ -10,6 +10,7 @@ import io.legado.app.App import io.legado.app.R import io.legado.app.help.BookHelp import io.legado.app.lib.theme.ATH +import io.legado.app.utils.LogUtils import io.legado.app.utils.getPrefString @@ -39,6 +40,7 @@ class ConfigFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChange override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { when (key) { "downloadPath" -> BookHelp.upDownloadPath() + "recordLog" -> LogUtils.upLevel() } } diff --git a/app/src/main/java/io/legado/app/ui/explore/ExploreShowAdapter.kt b/app/src/main/java/io/legado/app/ui/explore/ExploreShowAdapter.kt index 6510735f5..1a248e736 100644 --- a/app/src/main/java/io/legado/app/ui/explore/ExploreShowAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/explore/ExploreShowAdapter.kt @@ -59,8 +59,8 @@ class ExploreShowAdapter(context: Context, val callBack: CallBack) : } item.coverUrl.let { ImageLoader.load(context, it)//Glide自动识别http://和file:// - .placeholder(R.drawable.img_cover_default) - .error(R.drawable.img_cover_default) + .placeholder(R.drawable.image_cover_default) + .error(R.drawable.image_cover_default) .centerCrop() .setAsDrawable(iv_cover) } diff --git a/app/src/main/java/io/legado/app/ui/explore/ExploreShowViewModel.kt b/app/src/main/java/io/legado/app/ui/explore/ExploreShowViewModel.kt index f36a5fc69..a9514c8e4 100644 --- a/app/src/main/java/io/legado/app/ui/explore/ExploreShowViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/explore/ExploreShowViewModel.kt @@ -39,8 +39,6 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application App.db.searchBookDao().insert(*searchBooks.toTypedArray()) page++ } - }.onError { - it.printStackTrace() } } } diff --git a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt index 3a9c1db32..992ed6a16 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt @@ -38,9 +38,6 @@ class MainViewModel(application: Application) : BaseViewModel(application) { App.db.bookChapterDao().insert(*it.toTypedArray()) } } - .onError { - it.printStackTrace() - } .onFinally { synchronized(this) { updateList.remove(book.bookUrl) diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BooksAdapter.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BooksAdapter.kt index 603684163..5152278ca 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BooksAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BooksAdapter.kt @@ -35,7 +35,7 @@ class BooksAdapter(private val callBack: CallBack) : } fun notification(bookUrl: String) { - for (i in 0..itemCount) { + for (i in 0 until itemCount) { getItem(i)?.let { if (it.bookUrl == bookUrl) { notifyItemChanged(i) @@ -72,8 +72,8 @@ class BooksAdapter(private val callBack: CallBack) : tv_last.text = book.latestChapterTitle book.getDisplayCover()?.let { ImageLoader.load(context, it)//Glide自动识别http://和file:// - .placeholder(R.drawable.img_cover_default) - .error(R.drawable.img_cover_default) + .placeholder(R.drawable.image_cover_default) + .error(R.drawable.image_cover_default) .centerCrop() .setAsDrawable(iv_cover) } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BooksFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BooksFragment.kt index 4819d288f..47d168562 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BooksFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BooksFragment.kt @@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseFragment +import io.legado.app.constant.Bus import io.legado.app.data.entities.Book import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.accentColor @@ -20,6 +21,7 @@ import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.main.MainViewModel import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModelOfActivity +import io.legado.app.utils.observeEvent import kotlinx.android.synthetic.main.fragment_books.* import org.jetbrains.anko.startActivity @@ -52,6 +54,9 @@ class BooksFragment : VMBaseFragment(R.layout.fragment_books), } initRecyclerView() upRecyclerData() + observeEvent(Bus.UP_BOOK) { + booksAdapter.notification(it) + } } private fun initRecyclerView() { @@ -70,7 +75,6 @@ class BooksFragment : VMBaseFragment(R.layout.fragment_books), }) booksAdapter = BooksAdapter(this) rv_bookshelf.adapter = booksAdapter - } private fun upRecyclerData() { diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt index b7f91a0af..797ef096d 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt @@ -1,11 +1,9 @@ package io.legado.app.ui.main.bookshelf -import android.annotation.SuppressLint import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View -import android.widget.EditText import androidx.appcompat.widget.SearchView import androidx.lifecycle.LiveData import androidx.lifecycle.Observer @@ -16,11 +14,13 @@ import io.legado.app.R import io.legado.app.base.VMBaseFragment import io.legado.app.constant.AppConst import io.legado.app.data.entities.BookGroup -import io.legado.app.lib.dialogs.* +import io.legado.app.lib.dialogs.selector import io.legado.app.lib.theme.ATH import io.legado.app.ui.book.search.SearchActivity -import io.legado.app.utils.* -import kotlinx.android.synthetic.main.dialog_edit_text.view.* +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getViewModel +import io.legado.app.utils.putPrefInt +import io.legado.app.utils.startActivity import kotlinx.android.synthetic.main.fragment_bookshelf.* import kotlinx.android.synthetic.main.view_tab_layout.* import kotlinx.android.synthetic.main.view_title_bar.* @@ -28,13 +28,14 @@ import org.jetbrains.anko.startActivity class BookshelfFragment : VMBaseFragment(R.layout.fragment_bookshelf), SearchView.OnQueryTextListener, + GroupManageDialog.CallBack, BookshelfAdapter.CallBack { override val viewModel: BookshelfViewModel get() = getViewModel(BookshelfViewModel::class.java) private var bookGroupLiveData: LiveData>? = null - private val bookGroups = mutableListOf().apply { addAll(AppConst.defaultBookGroups) } + private val bookGroups = mutableListOf() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { setSupportToolbar(toolbar) @@ -51,6 +52,14 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b when (item.itemId) { R.id.menu_search -> startActivity() 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(R.layout.fragment_b bookGroupLiveData?.removeObservers(viewLifecycleOwner) bookGroupLiveData = App.db.bookGroupDao().liveDataAll() bookGroupLiveData?.observe(viewLifecycleOwner, Observer { - for (index in AppConst.defaultBookGroups.size until bookGroups.size) { - bookGroups.removeAt(AppConst.defaultBookGroups.size) + synchronized(this) { + bookGroups.clear() + bookGroups.add(AppConst.bookGroupAll) + if (getPrefBoolean("bookGroupLocal", true)) { + bookGroups.add(AppConst.bookGroupLocal) + } + if (getPrefBoolean("bookGroupAudio", true)) { + bookGroups.add(AppConst.bookGroupAudio) + } + bookGroups.addAll(it) + view_pager_bookshelf.adapter?.notifyDataSetChanged() } - bookGroups.addAll(it) - view_pager_bookshelf.adapter?.notifyDataSetChanged() }) } @@ -92,22 +108,18 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b return false } - @SuppressLint("InflateParams") - private fun showGroupInputDialog() { - alert(title = "新建分组") { - var editText: EditText? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - hint = "分组名称" - } - } + override fun upGroup() { + synchronized(this) { + bookGroups.remove(AppConst.bookGroupLocal) + bookGroups.remove(AppConst.bookGroupAudio) + if (getPrefBoolean("bookGroupAudio", true)) { + bookGroups.add(1, AppConst.bookGroupAudio) } - yesButton { - viewModel.saveBookGroup(editText?.text?.toString()) + if (getPrefBoolean("bookGroupLocal", true)) { + bookGroups.add(1, AppConst.bookGroupLocal) } - noButton() - }.show().applyTint().requestInputMethod() + view_pager_bookshelf.adapter?.notifyDataSetChanged() + } } private fun selectBookshelfLayout() { diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index 1bb9a3b64..0499e5e5e 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -7,17 +7,28 @@ import io.legado.app.data.entities.BookGroup class BookshelfViewModel(application: Application) : BaseViewModel(application) { - fun saveBookGroup(group: String?) { - if (!group.isNullOrBlank()) { - execute { - App.db.bookGroupDao().insert( - BookGroup( - App.db.bookGroupDao().maxId + 1, - group - ) - ) - } + + fun addGroup(groupName: String) { + execute { + val maxId = App.db.bookGroupDao().maxId + val bookGroup = BookGroup( + groupId = maxId.plus(1), + groupName = groupName, + order = maxId.plus(1) + ) + App.db.bookGroupDao().insert(bookGroup) + } + } + + fun upGroup(vararg bookGroup: BookGroup) { + execute { + App.db.bookGroupDao().update(*bookGroup) } } + fun delGroup(vararg bookGroup: BookGroup) { + execute { + App.db.bookGroupDao().delete(*bookGroup) + } + } } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageDialog.kt new file mode 100644 index 000000000..f6151edaf --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageDialog.kt @@ -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(context, R.layout.item_group_manage), + ItemTouchCallback.OnItemTouchCallbackListener { + + override fun convert(holder: ItemViewHolder, item: BookGroup, payloads: MutableList) { + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt index 6889124f3..ea23c0909 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt @@ -12,7 +12,7 @@ import io.legado.app.base.VMBaseFragment import io.legado.app.data.entities.BookSource import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.primaryTextColor -import io.legado.app.ui.book.source.edit.SourceEditActivity +import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.explore.ExploreShowActivity import io.legado.app.utils.getViewModel import io.legado.app.utils.startActivity @@ -88,7 +88,7 @@ class ExploreFragment : VMBaseFragment(R.layout.fragment_find_ } override fun editSource(sourceUrl: String) { - startActivity(Pair("data", sourceUrl)) + startActivity(Pair("data", sourceUrl)) } override fun toTop(source: BookSource) { diff --git a/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt b/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt index 36f2db63b..af839204e 100644 --- a/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt @@ -10,6 +10,7 @@ import androidx.preference.PreferenceFragmentCompat import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseFragment +import io.legado.app.help.BookHelp import io.legado.app.help.permission.Permissions import io.legado.app.help.permission.PermissionsCompat import io.legado.app.help.storage.Backup @@ -21,6 +22,7 @@ import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.config.ConfigActivity import io.legado.app.ui.config.ConfigViewModel import io.legado.app.ui.replacerule.ReplaceRuleActivity +import io.legado.app.utils.LogUtils import io.legado.app.utils.startActivity import kotlinx.android.synthetic.main.view_title_bar.* import org.jetbrains.anko.startActivity @@ -82,9 +84,9 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) { key: String? ) { when (key) { - "isNightTheme" -> { - App.INSTANCE.applyDayNight() - } + "isNightTheme" -> App.INSTANCE.applyDayNight() + "recordLog" -> LogUtils.upLevel() + "downloadPath" -> BookHelp.upDownloadPath() } } diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt index 6bef664c6..b490c8ec8 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt @@ -5,15 +5,26 @@ import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.RssSource +import io.legado.app.help.ImageLoader +import kotlinx.android.synthetic.main.item_rss.view.* +import org.jetbrains.anko.sdk27.listeners.onClick class RssAdapter(context: Context, val callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_rss) { override fun convert(holder: ItemViewHolder, item: RssSource, payloads: MutableList) { - + with(holder.itemView) { + tv_name.text = item.sourceName + ImageLoader.load(context, item.sourceIcon) + .centerCrop() + .placeholder(R.drawable.image_rss) + .error(R.drawable.image_rss) + .setAsBitmap(iv_icon) + onClick { callBack.openRss(item) } + } } interface CallBack { - fun openRss() + fun openRss(rssSource: RssSource) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt index 79c82bf1d..0dddd4426 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt @@ -1,15 +1,24 @@ package io.legado.app.ui.main.rss import android.os.Bundle +import android.util.Log +import android.view.Gravity import android.view.Menu import android.view.MenuItem import android.view.View +import android.widget.EditText +import android.widget.LinearLayout import androidx.lifecycle.Observer import androidx.recyclerview.widget.GridLayoutManager import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseFragment +import io.legado.app.data.entities.RssSource +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.cancelButton +import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.theme.ATH +import io.legado.app.ui.rss.article.RssArticlesActivity import io.legado.app.ui.rss.source.manage.RssSourceActivity import io.legado.app.utils.startActivity import kotlinx.android.synthetic.main.fragment_rss.* @@ -34,6 +43,25 @@ class RssFragment : BaseFragment(R.layout.fragment_rss), super.onCompatOptionsItemSelected(item) when (item.itemId) { R.id.menu_rss_config -> startActivity() + 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("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(Pair("url", rssSource.sourceUrl)) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt index 49fb8c3e6..2b2a84835 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt @@ -29,7 +29,6 @@ import io.legado.app.utils.requestInputMethod import io.legado.app.utils.splitNotBlank import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_recycler_view.* -import kotlinx.android.synthetic.main.item_book_group.view.tv_group import kotlinx.android.synthetic.main.item_group_manage.view.* import org.jetbrains.anko.sdk27.listeners.onClick diff --git a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt b/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt index 3fa28cd69..4b11ab39c 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt +++ b/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt @@ -25,7 +25,7 @@ class ReplaceEditDialog : DialogFragment(), val dialog = ReplaceEditDialog() id?.let { val bundle = Bundle() - bundle.putLong("data", id) + bundle.putLong("id", id) dialog.arguments = bundle } return dialog diff --git a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditViewModel.kt b/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditViewModel.kt index ae8ba8025..281a0ac2d 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditViewModel.kt @@ -14,7 +14,7 @@ class ReplaceEditViewModel(application: Application) : BaseViewModel(application fun initData(bundle: Bundle) { execute { replaceRuleData.value ?: let { - val id = bundle.getLong("data") + val id = bundle.getLong("id") if (id > 0) { App.db.replaceRuleDao().findById(id)?.let { replaceRuleData.postValue(it) diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesActivity.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesActivity.kt index a69c70fb5..8ec14de5b 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesActivity.kt @@ -1,3 +1,70 @@ package io.legado.app.ui.rss.article -class RssArticlesActivity \ No newline at end of file +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(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>? = 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( + Pair("description", rssArticle.description), + Pair("url", rssArticle.link) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt new file mode 100644 index 000000000..03546d957 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt @@ -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(context, R.layout.item_rss_article) { + + override fun convert(holder: ItemViewHolder, item: RssArticle, payloads: MutableList) { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt new file mode 100644 index 000000000..a75e7ae07 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt @@ -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() + + 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) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt index 5e7972c6d..eb8fa6451 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt @@ -3,13 +3,18 @@ package io.legado.app.ui.rss.read import android.os.Bundle import io.legado.app.R import io.legado.app.base.BaseActivity +import kotlinx.android.synthetic.main.activity_rss_read.* - -class ReadRssActivity : BaseActivity(R.layout.activity_read_rss) { +class ReadRssActivity : BaseActivity(R.layout.activity_rss_read) { override fun onActivityCreated(savedInstanceState: Bundle?) { - + val description = intent.getStringExtra("description") + val url = intent.getStringExtra("url") + if (description.isNullOrBlank()) { + webView.loadUrl(url) + } else { + webView.loadData("$description", "text/html", "utf-8") + } } - } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt new file mode 100644 index 000000000..6d43baf9a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt @@ -0,0 +1,2 @@ +package io.legado.app.ui.rss.read + diff --git a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugActivity.kt b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugActivity.kt index de8bf27dc..3c476c921 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugActivity.kt @@ -1,2 +1,16 @@ package io.legado.app.ui.rss.source.debug +import android.os.Bundle +import io.legado.app.R +import io.legado.app.base.BaseActivity + + +class RssSourceDebugActivity : BaseActivity(R.layout.activity_source_debug) { + + + override fun onActivityCreated(savedInstanceState: Bundle?) { + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt index e58ba568c..10e783e15 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt @@ -1,19 +1,212 @@ package io.legado.app.ui.rss.source.edit +import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.graphics.Rect import android.os.Bundle +import android.view.Gravity +import android.view.Menu +import android.view.MenuItem +import android.view.ViewTreeObserver +import android.widget.EditText +import android.widget.PopupWindow +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppConst +import io.legado.app.data.entities.EditEntity +import io.legado.app.data.entities.RssSource +import io.legado.app.lib.theme.ATH +import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity +import io.legado.app.ui.widget.KeyboardToolPop +import io.legado.app.utils.GSON import io.legado.app.utils.getViewModel +import kotlinx.android.synthetic.main.activity_book_source_edit.* +import org.jetbrains.anko.displayMetrics +import org.jetbrains.anko.startActivity +import org.jetbrains.anko.toast +import kotlin.math.abs class RssSourceEditActivity : - VMBaseActivity(R.layout.activity_rss_source_edit, false) { + VMBaseActivity(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 = ArrayList() override val viewModel: RssSourceEditViewModel get() = getViewModel(RssSourceEditViewModel::class.java) override fun onActivityCreated(savedInstanceState: Bundle?) { + initView() + viewModel.sourceLiveData.observe(this, Observer { + upRecyclerView(it) + }) + viewModel.initData(intent) + } + + override fun onDestroy() { + super.onDestroy() + mSoftKeyboardTool?.dismiss() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.source_edit, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_save -> { + getRssSource()?.let { + viewModel.save(it) { + setResult(Activity.RESULT_OK) + finish() + } + } + } + R.id.menu_debug_source -> { + getRssSource()?.let { + viewModel.save(it) { + startActivity(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() + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt new file mode 100644 index 000000000..78e0740ef --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt @@ -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() { + + var editEntities: ArrayList = 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) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt index fdb353c69..8b0f2cd11 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt @@ -1,9 +1,66 @@ package io.legado.app.ui.rss.source.edit import android.app.Application +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import io.legado.app.App import io.legado.app.base.BaseViewModel +import io.legado.app.data.entities.RssSource +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject class RssSourceEditViewModel(application: Application) : BaseViewModel(application) { + val sourceLiveData: MutableLiveData = 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(json)?.let { source -> + sourceLiveData.postValue(source) + } ?: toast("格式不对") + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/DiffCallBack.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/DiffCallBack.kt new file mode 100644 index 000000000..4260febe4 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/DiffCallBack.kt @@ -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, + private val newItems: List +) : 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt index 047a342d4..5515e4cf1 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt @@ -3,11 +3,16 @@ package io.legado.app.ui.rss.source.manage import android.os.Bundle import android.view.Menu import android.view.MenuItem +import android.view.SubMenu import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.data.entities.RssSource @@ -16,6 +21,7 @@ import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.primaryTextColor import io.legado.app.ui.rss.source.edit.RssSourceEditActivity import io.legado.app.utils.getViewModel +import io.legado.app.utils.splitNotBlank import kotlinx.android.synthetic.main.activity_rss_source.* import kotlinx.android.synthetic.main.view_search.* import kotlinx.android.synthetic.main.view_title_bar.* @@ -29,11 +35,16 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r get() = getViewModel(RssSourceViewModel::class.java) private lateinit var adapter: RssSourceAdapter + private var sourceLiveData: LiveData>? = null + private var groups = hashSetOf() + private var groupMenu: SubMenu? = null override fun onActivityCreated(savedInstanceState: Bundle?) { setSupportActionBar(toolbar) initRecyclerView() initSearchView() + initLiveDataGroup() + initLiveDataSource() } override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { @@ -41,9 +52,20 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r return super.onCompatCreateOptionsMenu(menu) } + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + groupMenu = menu?.findItem(R.id.menu_group)?.subMenu + upGroupMenu() + return super.onPrepareOptionsMenu(menu) + } + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.menu_add_book_source -> startActivity() + R.id.menu_add -> startActivity() + R.id.menu_select_all -> adapter.selectAll() + R.id.menu_revert_selection -> adapter.revertSelection() + R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelectionIds()) + R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelectionIds()) + R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelectionIds()) } return super.onCompatOptionsItemSelected(item) } @@ -81,24 +103,57 @@ class RssSourceActivity : VMBaseActivity(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(Pair("data", source.sourceUrl)) + } + override fun update(vararg source: RssSource) { + viewModel.update(*source) } override fun toTop(source: RssSource) { - + viewModel.topSource(source) } override fun upOrder() { - + viewModel.upOrder() } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt index 85aebdb18..8dee3c485 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt @@ -1,18 +1,95 @@ package io.legado.app.ui.rss.source.manage import android.content.Context +import android.view.Menu +import android.widget.PopupMenu import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.RssSource import io.legado.app.help.ItemTouchCallback +import io.legado.app.lib.theme.backgroundColor +import kotlinx.android.synthetic.main.item_rss_source.view.* +import org.jetbrains.anko.sdk27.listeners.onClick class RssSourceAdapter(context: Context, val callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_rss_source), ItemTouchCallback.OnItemTouchCallbackListener { - override fun convert(holder: ItemViewHolder, item: RssSource, payloads: MutableList) { + private val selectedIds = linkedSetOf() + + 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 { + val selection = linkedSetOf() + getItems().map { + if (selectedIds.contains(it.sourceUrl)) { + selection.add(it.sourceUrl) + } + } + return selection + } + + override fun convert(holder: ItemViewHolder, item: RssSource, payloads: MutableList) { + 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 + } + } + } } diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt index c91f73ef7..b0ad309d6 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt @@ -1,8 +1,52 @@ package io.legado.app.ui.rss.source.manage import android.app.Application +import io.legado.app.App import io.legado.app.base.BaseViewModel +import io.legado.app.data.entities.RssSource class RssSourceViewModel(application: Application) : BaseViewModel(application) { + fun topSource(rssSource: RssSource) { + execute { + rssSource.customOrder = App.db.rssSourceDao().minOrder - 1 + App.db.rssSourceDao().insert(rssSource) + } + } + + fun del(rssSource: RssSource) { + execute { App.db.rssSourceDao().delete(rssSource) } + } + + fun update(vararg rssSource: RssSource) { + execute { App.db.rssSourceDao().update(*rssSource) } + } + + fun upOrder() { + execute { + val sources = App.db.rssSourceDao().all + for ((index: Int, source: RssSource) in sources.withIndex()) { + source.customOrder = index + 1 + } + App.db.rssSourceDao().update(*sources.toTypedArray()) + } + } + + fun enableSelection(ids: LinkedHashSet) { + execute { + App.db.rssSourceDao().enableSection(*ids.toTypedArray()) + } + } + + fun disableSelection(ids: LinkedHashSet) { + execute { + App.db.rssSourceDao().disableSection(*ids.toTypedArray()) + } + } + + fun delSelection(ids: LinkedHashSet) { + execute { + App.db.rssSourceDao().delSection(*ids.toTypedArray()) + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt b/app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt index 57f40c039..40e321b87 100644 --- a/app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt +++ b/app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt @@ -18,7 +18,7 @@ import org.jetbrains.anko.sdk27.listeners.onClick class KeyboardToolPop( context: Context, private val chars: List, - val onClickListener: OnClickListener? + val callBack: CallBack? ) : PopupWindow(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) { init { @@ -45,13 +45,13 @@ class KeyboardToolPop( override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { with(holder.itemView) { text_view.text = item - onClick { onClickListener?.click(item) } + onClick { callBack?.sendText(item) } } } } - interface OnClickListener { - fun click(text: String) + interface CallBack { + fun sendText(text: String) } } diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/RefreshProgressBar.kt b/app/src/main/java/io/legado/app/ui/widget/anima/RefreshProgressBar.kt index 3eee366be..1095d5889 100644 --- a/app/src/main/java/io/legado/app/ui/widget/anima/RefreshProgressBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/anima/RefreshProgressBar.kt @@ -16,15 +16,15 @@ class RefreshProgressBar @JvmOverloads constructor( attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { - internal var a = 1 - var maxProgress = 100 + private var a = 1 private var durProgress = 0 - var secondMaxProgress = 100 private var secondDurProgress = 0 + var maxProgress = 100 + var secondMaxProgress = 100 var bgColor = 0x00000000 var secondColor = -0x3e3e3f var fontColor = -0xc9c9ca - var speed = 1 + var speed = 2 var secondFinalProgress = 0 private set private var paint: Paint = Paint() diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/Utils.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/Utils.kt index 2f99191d2..6e33941fb 100644 --- a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/Utils.kt +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/Utils.kt @@ -22,6 +22,7 @@ import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.view.View import android.widget.ImageView +import kotlin.math.roundToInt object Utils { @@ -29,7 +30,7 @@ object Utils { private val sCanvas = Canvas() fun dp2Px(dp: Int): Int { - return Math.round(dp * DENSITY) + return (dp * DENSITY).roundToInt() } fun createBitmapFromView(view: View): Bitmap? { @@ -55,7 +56,7 @@ object Utils { return bitmap } - fun createBitmapSafely( + private fun createBitmapSafely( width: Int, height: Int, config: Bitmap.Config, diff --git a/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt index bb6a4461e..bcb449329 100644 --- a/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt @@ -7,6 +7,8 @@ import android.graphics.Path import android.util.AttributeSet import androidx.appcompat.widget.AppCompatImageView import io.legado.app.R +import io.legado.app.utils.dp +import kotlin.math.max class FilletImageView : AppCompatImageView { internal var width: Float = 0.toFloat() @@ -29,7 +31,7 @@ class FilletImageView : AppCompatImageView { private fun init(context: Context, attrs: AttributeSet) { // 读取配置 val array = context.obtainStyledAttributes(attrs, R.styleable.FilletImageView) - val defaultRadius = 5 + val defaultRadius = 5.dp val radius = array.getDimensionPixelOffset(R.styleable.FilletImageView_radius, defaultRadius) leftTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_left_top_radius, defaultRadius) rightTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_right_top_radius, defaultRadius) @@ -62,11 +64,11 @@ class FilletImageView : AppCompatImageView { override fun onDraw(canvas: Canvas) { //这里做下判断,只有图片的宽高大于设置的圆角距离的时候才进行裁剪 - val maxLeft = Math.max(leftTopRadius, leftBottomRadius) - val maxRight = Math.max(rightTopRadius, rightBottomRadius) + val maxLeft = max(leftTopRadius, leftBottomRadius) + val maxRight = max(rightTopRadius, rightBottomRadius) val minWidth = maxLeft + maxRight - val maxTop = Math.max(leftTopRadius, rightTopRadius) - val maxBottom = Math.max(leftBottomRadius, rightBottomRadius) + val maxTop = max(leftTopRadius, rightTopRadius) + val maxBottom = max(leftBottomRadius, rightBottomRadius) val minHeight = maxTop + maxBottom if (width >= minWidth && height > minHeight) { @SuppressLint("DrawAllocation") val path = Path() diff --git a/app/src/main/java/io/legado/app/ui/widget/page/ContentView.kt b/app/src/main/java/io/legado/app/ui/widget/page/ContentView.kt index 719129a03..228e6f2e9 100644 --- a/app/src/main/java/io/legado/app/ui/widget/page/ContentView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/page/ContentView.kt @@ -10,7 +10,6 @@ import android.widget.ImageView import androidx.appcompat.widget.AppCompatImageView import io.legado.app.R import io.legado.app.constant.AppConst.TIME_FORMAT -import io.legado.app.help.ImageLoader import io.legado.app.help.ReadBookConfig import io.legado.app.utils.* import kotlinx.android.synthetic.main.view_book_page.view.* @@ -90,10 +89,7 @@ class ContentView : FrameLayout { } fun setBg(bg: Drawable?) { - //all supported - ImageLoader.load(context, bg) - .centerCrop() - .setAsDrawable(bgImage) + bgImage.background = bg } fun upTime() { diff --git a/app/src/main/java/io/legado/app/utils/ACache.kt b/app/src/main/java/io/legado/app/utils/ACache.kt index 5ac75f875..7549813e8 100644 --- a/app/src/main/java/io/legado/app/utils/ACache.kt +++ b/app/src/main/java/io/legado/app/utils/ACache.kt @@ -23,7 +23,49 @@ import kotlin.math.min * 本地缓存 */ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) { - private lateinit var mCache: ACacheManager + + + companion object { + const val TIME_HOUR = 60 * 60 + const val TIME_DAY = TIME_HOUR * 24 + private const val MAX_SIZE = 1000 * 1000 * 50 // 50 mb + private const val MAX_COUNT = Integer.MAX_VALUE // 不限制存放数据的数量 + private val mInstanceMap = HashMap() + + @JvmOverloads + fun get( + ctx: Context, + cacheName: String = "ACache", + maxSize: Long = MAX_SIZE.toLong(), + maxCount: Int = MAX_COUNT, + cacheDir: Boolean = true + ): ACache { + val f = if (cacheDir) File(ctx.cacheDir, cacheName) else File(ctx.filesDir, cacheName) + return get(f, maxSize, maxCount) + } + + @JvmOverloads + fun get( + cacheDir: File, + maxSize: Long = MAX_SIZE.toLong(), + maxCount: Int = MAX_COUNT + ): ACache { + synchronized(this) { + var manager = mInstanceMap[cacheDir.absoluteFile.toString() + myPid()] + if (manager == null) { + manager = ACache(cacheDir, maxSize, maxCount) + mInstanceMap[cacheDir.absolutePath + myPid()] = manager + } + return manager + } + } + + private fun myPid(): String { + return "_" + android.os.Process.myPid() + } + } + + private var mCache: ACacheManager? = null init { try { @@ -48,12 +90,14 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) * @param value 保存的String数据 */ fun put(key: String, value: String) { - try { - val file = mCache.newFile(key) - file.writeText(value) - mCache.put(file) - } catch (e: Exception) { - e.printStackTrace() + mCache?.let { mCache -> + try { + val file = mCache.newFile(key) + file.writeText(value) + mCache.put(file) + } catch (e: Exception) { + e.printStackTrace() + } } } @@ -74,24 +118,26 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) * @return String 数据 */ fun getAsString(key: String): String? { - val file = mCache[key] - if (!file.exists()) - return null - var removeFile = false - return try { - val text = file.readText() - if (!Utils.isDue(text)) { - Utils.clearDateInfo(text) - } else { - removeFile = true - null + mCache?.let { mCache -> + val file = mCache[key] + if (!file.exists()) + return null + var removeFile = false + try { + val text = file.readText() + if (!Utils.isDue(text)) { + return Utils.clearDateInfo(text) + } else { + 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 保存的数据 */ fun put(key: String, value: ByteArray) { - val file = mCache.newFile(key) - file.writeBytes(value) - mCache.put(file) + mCache?.let { mCache -> + val file = mCache.newFile(key) + file.writeBytes(value) + mCache.put(file) + } } /** @@ -206,26 +254,28 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) * @return byte 数据 */ fun getAsBinary(key: String): ByteArray? { - var removeFile = false - try { - val file = mCache[key] - if (!file.exists()) - return null + mCache?.let { mCache -> + var removeFile = false + try { + val file = mCache[key] + if (!file.exists()) + return null - val byteArray = file.readBytes() - return if (!Utils.isDue(byteArray)) { - Utils.clearDateInfo(byteArray) - } else { - removeFile = true - null + val byteArray = file.readBytes() + return if (!Utils.isDue(byteArray)) { + Utils.clearDateInfo(byteArray) + } else { + removeFile = true + 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() } catch (e: Exception) { e.printStackTrace() - return null } finally { try { bais?.close() @@ -371,15 +420,16 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) * @return value 缓存的文件 */ fun file(key: String): File? { - try { - val f = mCache.newFile(key) - if (f.exists()) { - return f + mCache?.let { mCache -> + try { + val f = mCache.newFile(key) + if (f.exists()) { + return f + } + } catch (e: Exception) { + e.printStackTrace() } - } catch (e: Exception) { - e.printStackTrace() } - return null } @@ -389,14 +439,14 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) * @return 是否移除成功 */ fun remove(key: String): Boolean { - return mCache.remove(key) + return mCache?.remove(key) == true } /** * 清除所有数据 */ fun clear() { - mCache.clear() + mCache?.clear() } /** @@ -726,44 +776,4 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) } } - companion object { - const val TIME_HOUR = 60 * 60 - const val TIME_DAY = TIME_HOUR * 24 - private const val MAX_SIZE = 1000 * 1000 * 50 // 50 mb - private const val MAX_COUNT = Integer.MAX_VALUE // 不限制存放数据的数量 - private val mInstanceMap = HashMap() - - @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() - } - } - } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/BitmapUtil.kt b/app/src/main/java/io/legado/app/utils/BitmapUtil.kt new file mode 100644 index 000000000..2b72e17db --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/BitmapUtil.kt @@ -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 + } + +} diff --git a/app/src/main/java/io/legado/app/utils/LogUtils.kt b/app/src/main/java/io/legado/app/utils/LogUtils.kt index 571c4e4cf..3dd8f3dfe 100644 --- a/app/src/main/java/io/legado/app/utils/LogUtils.kt +++ b/app/src/main/java/io/legado/app/utils/LogUtils.kt @@ -11,66 +11,52 @@ import java.util.logging.Formatter object LogUtils { + const val TIME_PATTERN = "yyyy-MM-dd HH:mm:ss" - private val logger: Logger by lazy { - Logger.getGlobal().apply { - addFileHandler( - this, - Level.INFO, - FileHelp.getCachePath() - ) - } + @JvmStatic + fun d(tag: String, msg: String) { + logger.log(Level.INFO, "$tag $msg") } - fun log(msg: String) { - if (App.INSTANCE.getPrefBoolean("recordLog")) { - logger.log(Level.INFO, msg) - } + @JvmStatic + fun e(tag: String, msg: String) { + logger.log(Level.WARNING, "$tag $msg") } - private const val DATE_PATTERN = "yyyy-MM-dd" - const val TIME_PATTERN = "HH:mm:ss" - - /** - * 为log添加控制台handler - * - * @param log 要添加handler的log - * @param level 控制台的输出等级 - */ - fun addConsoleHandler(log: Logger, level: Level) { - // 控制台输出的handler - val consoleHandler = ConsoleHandler() - // 设置控制台输出的等级(如果ConsoleHandler的等级高于或者等于log的level,则按照FileHandler的level输出到控制台,如果低于,则按照Log等级输出) - consoleHandler.level = level - // 添加控制台的handler - log.addHandler(consoleHandler) + private val logger: Logger by lazy { + Logger.getGlobal().apply { + addHandler(fileHandler) + } } - /** - * 为log添加文件输出Handler - * - * @param log 要添加文件输出handler的log - * @param level log输出等级 - * @param filePath 指定文件全路径 - */ - fun addFileHandler(log: Logger, level: Level, filePath: String) { - var fileHandler: FileHandler? = null - try { - fileHandler = - FileHandler(filePath + File.separator + getCurrentDateStr(DATE_PATTERN) + ".log") - // 设置输出文件的等级(如果FileHandler的等级高于或者等于log的level,则按照FileHandler的level输出到文件,如果低于,则按照Log等级输出) - fileHandler.level = level - fileHandler.formatter = object : Formatter() { + private val fileHandler by lazy { + val logFolder = FileHelp.getCachePath() + File.separator + "logs" + FileHelp.getFolder(logFolder) + FileHandler( + logFolder + File.separator + "app.log", + 2048, + 10 + ).apply { + formatter = object : Formatter() { override fun format(record: LogRecord): String { // 设置文件输出格式 - return (getCurrentDateStr(TIME_PATTERN) + ":" + record.message + "\n") + return (getCurrentDateStr(TIME_PATTERN) + ": " + record.message + "\n") } } - } catch (e: Exception) { - e.printStackTrace() + level = if (App.INSTANCE.getPrefBoolean("recordLog")) { + Level.INFO + } else { + Level.OFF + } + } + } + + fun upLevel() { + fileHandler.level = if (App.INSTANCE.getPrefBoolean("recordLog")) { + Level.INFO + } else { + Level.OFF } - // 添加输出文件handler - log.addHandler(fileHandler) } /** diff --git a/app/src/main/res/drawable/img_cover_default.jpg b/app/src/main/res/drawable/image_cover_default.jpg similarity index 100% rename from app/src/main/res/drawable/img_cover_default.jpg rename to app/src/main/res/drawable/image_cover_default.jpg diff --git a/app/src/main/res/drawable/img_cover_gs.jpg b/app/src/main/res/drawable/image_cover_gs.jpg similarity index 100% rename from app/src/main/res/drawable/img_cover_gs.jpg rename to app/src/main/res/drawable/image_cover_gs.jpg diff --git a/app/src/main/res/drawable/image_rss.jpg b/app/src/main/res/drawable/image_rss.jpg new file mode 100644 index 000000000..a152df506 Binary files /dev/null and b/app/src/main/res/drawable/image_rss.jpg differ diff --git a/app/src/main/res/layout-land/activity_book_info.xml b/app/src/main/res/layout-land/activity_book_info.xml index a5419f1e6..a284835eb 100644 --- a/app/src/main/res/layout-land/activity_book_info.xml +++ b/app/src/main/res/layout-land/activity_book_info.xml @@ -28,7 +28,7 @@ android:layout_width="86dp" android:layout_height="120dp" android:layout_margin="10dp" - android:src="@drawable/img_cover_default" + android:src="@drawable/image_cover_default" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@+id/title_bar" /> @@ -165,7 +165,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="8dp" - android:text="简介" + android:text="@string/book_intro" android:textColor="@color/tv_text_default" android:textSize="16sp" /> @@ -192,22 +192,15 @@ app:layout_constraintRight_toRightOf="parent" app:layout_constraintLeft_toRightOf="@+id/fg"> - - - - - + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="8dp" + android:text="@string/chapter_list" + android:textColor="@color/tv_text_default" + android:textSize="16sp" /> + android:text="@string/dur_pos" /> + + diff --git a/app/src/main/res/layout/activity_audio_play.xml b/app/src/main/res/layout/activity_audio_play.xml index 2a392be02..ec6bbd556 100644 --- a/app/src/main/res/layout/activity_audio_play.xml +++ b/app/src/main/res/layout/activity_audio_play.xml @@ -25,7 +25,7 @@ android:layout_height="260dp" android:layout_gravity="center" android:contentDescription="@string/img_cover" - android:src="@drawable/img_cover_default" + android:src="@drawable/image_cover_default" app:layout_constraintTop_toBottomOf="@+id/title_bar" app:layout_constraintBottom_toTopOf="@+id/ll_player_progress" app:layout_constraintLeft_toLeftOf="parent" diff --git a/app/src/main/res/layout/activity_book_info.xml b/app/src/main/res/layout/activity_book_info.xml index 2ccef7e17..9c2887c68 100644 --- a/app/src/main/res/layout/activity_book_info.xml +++ b/app/src/main/res/layout/activity_book_info.xml @@ -19,7 +19,7 @@ android:layout_height="120dp" android:layout_margin="10dp" android:scaleType="centerCrop" - android:src="@drawable/img_cover_default" + android:src="@drawable/image_cover_default" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@+id/title_bar" /> @@ -154,7 +154,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="8dp" - android:text="简介" + android:text="@string/book_intro" android:textColor="@color/tv_text_default" android:textSize="16sp" /> @@ -184,22 +184,15 @@ android:layout_height="5dp" android:background="@color/bg_divider_line" /> - - - - - + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="8dp" + android:text="@string/chapter_list" + android:textColor="@color/tv_text_default" + android:textSize="16sp" /> + android:text="@string/dur_pos" /> + + @@ -284,8 +287,6 @@ android:layout_height="match_parent" android:layout_weight="1" android:background="@drawable/selector_btn_accent_bg" - android:clickable="true" - android:focusable="true" android:gravity="center" android:includeFontPadding="false" android:text="@string/reading" diff --git a/app/src/main/res/layout/activity_book_info_edit.xml b/app/src/main/res/layout/activity_book_info_edit.xml index 4244c2b89..12328a069 100644 --- a/app/src/main/res/layout/activity_book_info_edit.xml +++ b/app/src/main/res/layout/activity_book_info_edit.xml @@ -34,7 +34,7 @@ android:layout_height="126dp" android:contentDescription="@string/img_cover" android:scaleType="centerCrop" - android:src="@drawable/img_cover_default" /> + android:src="@drawable/image_cover_default" /> + + + + + + + + + + + + + + + + + + - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_rss_artivles.xml b/app/src/main/res/layout/activity_rss_artivles.xml new file mode 100644 index 000000000..5eceec843 --- /dev/null +++ b/app/src/main/res/layout/activity_rss_artivles.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_rss_read.xml b/app/src/main/res/layout/activity_rss_read.xml new file mode 100644 index 000000000..5d403f5fe --- /dev/null +++ b/app/src/main/res/layout/activity_rss_read.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_rss_source_edit.xml b/app/src/main/res/layout/activity_rss_source_edit.xml index 437330f50..dd2545faf 100644 --- a/app/src/main/res/layout/activity_rss_source_edit.xml +++ b/app/src/main/res/layout/activity_rss_source_edit.xml @@ -1,6 +1,7 @@ @@ -11,7 +12,12 @@ android:layout_height="wrap_content" app:contentInsetStartWithNavigation="0dp" app:displayHomeAsUp="true" + app:fitStatusBar="false" app:title="@string/rss_source_edit" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_book_source_debug.xml b/app/src/main/res/layout/activity_source_debug.xml similarity index 100% rename from app/src/main/res/layout/activity_book_source_debug.xml rename to app/src/main/res/layout/activity_source_debug.xml diff --git a/app/src/main/res/layout/fragment_rss.xml b/app/src/main/res/layout/fragment_rss.xml index 3d9e9a4b5..f5a3b374c 100644 --- a/app/src/main/res/layout/fragment_rss.xml +++ b/app/src/main/res/layout/fragment_rss.xml @@ -11,7 +11,6 @@ android:layout_height="wrap_content" app:attachToActivity="false" app:title="@string/rss" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_bookshelf_list.xml b/app/src/main/res/layout/item_bookshelf_list.xml index 03fd1afc1..b1b0f45a9 100644 --- a/app/src/main/res/layout/item_bookshelf_list.xml +++ b/app/src/main/res/layout/item_bookshelf_list.xml @@ -16,7 +16,7 @@ android:layout_margin="8dp" android:contentDescription="@string/img_cover" android:scaleType="centerCrop" - android:src="@drawable/img_cover_default" + android:src="@drawable/image_cover_default" android:transitionName="img_cover" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" diff --git a/app/src/main/res/layout/item_group_manage.xml b/app/src/main/res/layout/item_group_manage.xml index 493bf7998..d7d85630a 100644 --- a/app/src/main/res/layout/item_group_manage.xml +++ b/app/src/main/res/layout/item_group_manage.xml @@ -2,6 +2,7 @@ diff --git a/app/src/main/res/layout/item_rss.xml b/app/src/main/res/layout/item_rss.xml index d829e291c..8bf74003e 100644 --- a/app/src/main/res/layout/item_rss.xml +++ b/app/src/main/res/layout/item_rss.xml @@ -1,7 +1,26 @@ - + android:layout_height="wrap_content"> - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_rss_article.xml b/app/src/main/res/layout/item_rss_article.xml new file mode 100644 index 000000000..293ae0ce3 --- /dev/null +++ b/app/src/main/res/layout/item_rss_article.xml @@ -0,0 +1,48 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_rss_source.xml b/app/src/main/res/layout/item_rss_source.xml index 7d9513d98..d0ecd8788 100644 --- a/app/src/main/res/layout/item_rss_source.xml +++ b/app/src/main/res/layout/item_rss_source.xml @@ -9,7 +9,7 @@ android:padding="16dp"> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_bookshelf.xml b/app/src/main/res/menu/main_bookshelf.xml index 19bbbdb7f..45b586ca8 100644 --- a/app/src/main/res/menu/main_bookshelf.xml +++ b/app/src/main/res/menu/main_bookshelf.xml @@ -26,6 +26,12 @@ android:title="@string/download_all" app:showAsAction="never" /> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/pref_key_value.xml b/app/src/main/res/values/pref_key_value.xml index dd706603a..091d276ad 100644 --- a/app/src/main/res/values/pref_key_value.xml +++ b/app/src/main/res/values/pref_key_value.xml @@ -19,7 +19,7 @@ Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.2357.134 Safari/537.36 https://gedoor.github.io/MyBookshelf/sourcerule.html - https://github.com/gedoor/MyBookshelf + https://github.com/gedoor/legado https://gedoor.github.io/MyBookshelf/disclaimer.html https://gedoor.github.io/MyBookshelf/ https://github.com/gedoor/MyBookshelf/releases/latest diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 72667cbab..52525e7cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,6 +35,7 @@ 替换净化-搜索 书架 订阅 + 全部 最近阅读 最后阅读 让阅读成为一种习惯。 @@ -149,9 +150,9 @@ 新建订阅源 添加书籍 扫描 - 拷贝书源 - 粘贴书源 - 书源规则说明 + 拷贝源 + 粘贴源 + 源规则说明 检查更新 扫描二维码 扫描本地图片 @@ -374,6 +375,23 @@ 正文下一页URL规则(nextContentUrl) + + 名称(sourceName) + url(sourceUrl) + 图标(sourceIcon) + 分组(sourceGroup) + 列表规则(ruleArticles) + 标题规则(ruleTitle) + 作者规则(ruleAuthor) + guid规则(ruleGuid) + 时间规则(rulePubDate) + 类别规则(ruleCategories) + 描述规则(ruleDescription) + 图片url规则(ruleImage) + 内容规则(ruleContent) + 链接规则(ruleLink) + + 没有书源 书籍信息获取失败 @@ -390,7 +408,7 @@ header - 调试书源 + 调试源 二维码导入 扫描二维码 选中时点击可弹出菜单 @@ -508,5 +526,6 @@ 编辑订阅源 筛选 筛选发现 + 当前位置: diff --git a/build.gradle b/build.gradle index 3c95124c7..bfcb4cc7a 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { maven { url 'https://plugins.gradle.org/m2/' } } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:3.5.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'de.timfreiheit.resourceplaceholders:placeholders:0.3' }