commit
						8be436f6c4
					
				| @ -0,0 +1,18 @@ | ||||
| package io.legado.app.data.dao | ||||
| 
 | ||||
| import androidx.lifecycle.LiveData | ||||
| import androidx.room.* | ||||
| import io.legado.app.data.entities.RssArticle | ||||
| 
 | ||||
| @Dao | ||||
| interface RssArticleDao { | ||||
| 
 | ||||
|     @Query("select * from rssArticles where origin = :origin order by time desc") | ||||
|     fun liveByOrigin(origin: String): LiveData<List<RssArticle>> | ||||
| 
 | ||||
|     @Insert(onConflict = OnConflictStrategy.IGNORE) | ||||
|     fun insert(vararg rssArticle: RssArticle) | ||||
| 
 | ||||
|     @Update | ||||
|     fun update(vararg rssArticle: RssArticle) | ||||
| } | ||||
| @ -0,0 +1,3 @@ | ||||
| package io.legado.app.data.entities | ||||
| 
 | ||||
| data class EditEntity(var key: String, var value: String?, var hint: Int) | ||||
| @ -0,0 +1,24 @@ | ||||
| package io.legado.app.model | ||||
| 
 | ||||
| import io.legado.app.data.entities.RssArticle | ||||
| import io.legado.app.data.entities.RssSource | ||||
| import io.legado.app.help.coroutine.Coroutine | ||||
| import io.legado.app.model.analyzeRule.AnalyzeUrl | ||||
| import io.legado.app.model.rss.RssParserByRule | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlin.coroutines.CoroutineContext | ||||
| 
 | ||||
| object Rss { | ||||
| 
 | ||||
|     fun getArticles( | ||||
|         rssSource: RssSource, | ||||
|         scope: CoroutineScope = Coroutine.DEFAULT, | ||||
|         context: CoroutineContext = Dispatchers.IO | ||||
|     ): Coroutine<MutableList<RssArticle>> { | ||||
|         return Coroutine.async(scope, context) { | ||||
|             val response = AnalyzeUrl(rssSource.sourceUrl).getResponseAsync().await() | ||||
|             RssParserByRule.parseXML(response, rssSource) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,94 @@ | ||||
| package io.legado.app.model.rss | ||||
| 
 | ||||
| import io.legado.app.App | ||||
| import io.legado.app.R | ||||
| import io.legado.app.data.entities.RssArticle | ||||
| import io.legado.app.data.entities.RssSource | ||||
| import io.legado.app.model.analyzeRule.AnalyzeRule | ||||
| import retrofit2.Response | ||||
| 
 | ||||
| object RssParserByRule { | ||||
| 
 | ||||
|     @Throws(Exception::class) | ||||
|     fun parseXML(response: Response<String>, rssSource: RssSource): MutableList<RssArticle> { | ||||
|         val articleList = mutableListOf<RssArticle>() | ||||
| 
 | ||||
|         val xml = response.body() | ||||
|         if (xml.isNullOrBlank()) { | ||||
|             throw Exception( | ||||
|                 App.INSTANCE.getString( | ||||
|                     R.string.error_get_web_content, | ||||
|                     rssSource.sourceUrl | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         rssSource.ruleArticles?.let { ruleArticles -> | ||||
|             val analyzeRule = AnalyzeRule() | ||||
|             analyzeRule.setContent(xml) | ||||
|             val collections = analyzeRule.getElements(ruleArticles) | ||||
|             val ruleGuid = analyzeRule.splitSourceRule(rssSource.ruleGuid ?: "") | ||||
|             val ruleTitle = analyzeRule.splitSourceRule(rssSource.ruleTitle ?: "") | ||||
|             val ruleAuthor = analyzeRule.splitSourceRule(rssSource.ruleAuthor ?: "") | ||||
|             val rulePubDate = analyzeRule.splitSourceRule(rssSource.rulePubDate ?: "") | ||||
|             val ruleCategories = analyzeRule.splitSourceRule(rssSource.ruleCategories ?: "") | ||||
|             val ruleDescription = analyzeRule.splitSourceRule(rssSource.ruleDescription ?: "") | ||||
|             val ruleImage = analyzeRule.splitSourceRule(rssSource.ruleImage ?: "") | ||||
|             val ruleContent = analyzeRule.splitSourceRule(rssSource.ruleContent ?: "") | ||||
|             val ruleLink = analyzeRule.splitSourceRule(rssSource.ruleLink ?: "") | ||||
|             for ((index, item) in collections.withIndex()) { | ||||
|                 getItem( | ||||
|                     item, | ||||
|                     analyzeRule, | ||||
|                     index == 0, | ||||
|                     ruleGuid, | ||||
|                     ruleTitle, | ||||
|                     ruleAuthor, | ||||
|                     rulePubDate, | ||||
|                     ruleCategories, | ||||
|                     ruleDescription, | ||||
|                     ruleImage, | ||||
|                     ruleContent, | ||||
|                     ruleLink | ||||
|                 )?.let { | ||||
|                     it.origin = rssSource.sourceUrl | ||||
|                     articleList.add(it) | ||||
|                 } | ||||
|             } | ||||
|         } ?: let { | ||||
|             return RssParser.parseXML(xml, rssSource.sourceUrl) | ||||
|         } | ||||
|         return articleList | ||||
|     } | ||||
| 
 | ||||
|     private fun getItem( | ||||
|         item: Any, | ||||
|         analyzeRule: AnalyzeRule, | ||||
|         printLog: Boolean, | ||||
|         ruleGuid: List<AnalyzeRule.SourceRule>, | ||||
|         ruleTitle: List<AnalyzeRule.SourceRule>, | ||||
|         ruleAuthor: List<AnalyzeRule.SourceRule>, | ||||
|         rulePubDate: List<AnalyzeRule.SourceRule>, | ||||
|         ruleCategories: List<AnalyzeRule.SourceRule>, | ||||
|         ruleDescription: List<AnalyzeRule.SourceRule>, | ||||
|         ruleImage: List<AnalyzeRule.SourceRule>, | ||||
|         ruleContent: List<AnalyzeRule.SourceRule>, | ||||
|         ruleLink: List<AnalyzeRule.SourceRule> | ||||
|     ): RssArticle? { | ||||
|         val rssArticle = RssArticle() | ||||
|         analyzeRule.setContent(item) | ||||
|         rssArticle.guid = analyzeRule.getString(ruleGuid) | ||||
|         rssArticle.title = analyzeRule.getString(ruleTitle) | ||||
|         rssArticle.author = analyzeRule.getString(ruleAuthor) | ||||
|         rssArticle.pubDate = analyzeRule.getString(rulePubDate) | ||||
|         rssArticle.categories = analyzeRule.getString(ruleCategories) | ||||
|         rssArticle.description = analyzeRule.getString(ruleDescription) | ||||
|         rssArticle.image = analyzeRule.getString(ruleImage) | ||||
|         rssArticle.link = analyzeRule.getString(ruleLink) | ||||
|         rssArticle.content = analyzeRule.getString(ruleContent) | ||||
|         if (rssArticle.title.isNullOrBlank()) { | ||||
|             return null | ||||
|         } | ||||
|         return rssArticle | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,24 @@ | ||||
| package io.legado.app.ui.book.search | ||||
| 
 | ||||
| import android.content.Context | ||||
| import io.legado.app.R | ||||
| import io.legado.app.base.adapter.ItemViewHolder | ||||
| import io.legado.app.base.adapter.SimpleRecyclerAdapter | ||||
| import io.legado.app.data.entities.Book | ||||
| import kotlinx.android.synthetic.main.item_text.view.* | ||||
| import org.jetbrains.anko.sdk27.listeners.onClick | ||||
| 
 | ||||
| class BookAdapter(context: Context, val callBack: CallBack) : | ||||
|     SimpleRecyclerAdapter<Book>(context, R.layout.item_text) { | ||||
| 
 | ||||
|     override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) { | ||||
|         with(holder.itemView) { | ||||
|             text_view.text = item.name | ||||
|             onClick { callBack.showBookInfo(item.bookUrl) } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     interface CallBack { | ||||
|         fun showBookInfo(url: String) | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,42 @@ | ||||
| package io.legado.app.ui.book.search | ||||
| 
 | ||||
| import android.content.Context | ||||
| import io.legado.app.App | ||||
| import io.legado.app.R | ||||
| import io.legado.app.base.adapter.ItemViewHolder | ||||
| import io.legado.app.base.adapter.SimpleRecyclerAdapter | ||||
| import io.legado.app.data.entities.SearchKeyword | ||||
| import io.legado.app.ui.widget.anima.explosion_field.ExplosionField | ||||
| import kotlinx.android.synthetic.main.item_text.view.* | ||||
| import kotlinx.coroutines.Dispatchers.IO | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.launch | ||||
| import org.jetbrains.anko.sdk27.listeners.onClick | ||||
| import org.jetbrains.anko.sdk27.listeners.onLongClick | ||||
| 
 | ||||
| 
 | ||||
| class HistoryKeyAdapter(context: Context, val callBack: CallBack) : | ||||
|     SimpleRecyclerAdapter<SearchKeyword>(context, R.layout.item_text) { | ||||
| 
 | ||||
|     override fun convert(holder: ItemViewHolder, item: SearchKeyword, payloads: MutableList<Any>) { | ||||
|         with(holder.itemView) { | ||||
|             text_view.text = item.word | ||||
|             onClick { | ||||
|                 callBack.searchHistory(item.word) | ||||
|             } | ||||
|             onLongClick { | ||||
|                 it?.let { | ||||
|                     ExplosionField(context).explode(it, true) | ||||
|                 } | ||||
|                 GlobalScope.launch(IO) { | ||||
|                     App.db.searchKeywordDao().delete(item) | ||||
|                 } | ||||
|                 true | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     interface CallBack { | ||||
|         fun searchHistory(key: String) | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,369 @@ | ||||
| package io.legado.app.ui.book.source.edit | ||||
| 
 | ||||
| import android.app.Activity | ||||
| import android.content.ClipData | ||||
| import android.content.ClipboardManager | ||||
| import android.content.Context | ||||
| import android.graphics.Rect | ||||
| import android.os.Bundle | ||||
| import android.view.Gravity | ||||
| import android.view.Menu | ||||
| import android.view.MenuItem | ||||
| import android.view.ViewTreeObserver | ||||
| import android.widget.EditText | ||||
| import android.widget.PopupWindow | ||||
| import androidx.lifecycle.Observer | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import com.google.android.material.tabs.TabLayout | ||||
| import io.legado.app.R | ||||
| import io.legado.app.base.VMBaseActivity | ||||
| import io.legado.app.constant.AppConst | ||||
| import io.legado.app.data.entities.BookSource | ||||
| import io.legado.app.data.entities.EditEntity | ||||
| import io.legado.app.data.entities.rule.* | ||||
| import io.legado.app.lib.theme.ATH | ||||
| import io.legado.app.ui.book.source.debug.BookSourceDebugActivity | ||||
| import io.legado.app.ui.widget.KeyboardToolPop | ||||
| import io.legado.app.utils.GSON | ||||
| import io.legado.app.utils.getViewModel | ||||
| import kotlinx.android.synthetic.main.activity_book_source_edit.* | ||||
| import org.jetbrains.anko.displayMetrics | ||||
| import org.jetbrains.anko.startActivity | ||||
| import org.jetbrains.anko.toast | ||||
| import kotlin.math.abs | ||||
| 
 | ||||
| class BookSourceEditActivity : | ||||
|     VMBaseActivity<BookSourceEditViewModel>(R.layout.activity_book_source_edit, false), | ||||
|     KeyboardToolPop.CallBack { | ||||
|     override val viewModel: BookSourceEditViewModel | ||||
|         get() = getViewModel(BookSourceEditViewModel::class.java) | ||||
| 
 | ||||
|     private val adapter = BookSourceEditAdapter() | ||||
|     private val sourceEntities: ArrayList<EditEntity> = ArrayList() | ||||
|     private val searchEntities: ArrayList<EditEntity> = ArrayList() | ||||
|     private val findEntities: ArrayList<EditEntity> = ArrayList() | ||||
|     private val infoEntities: ArrayList<EditEntity> = ArrayList() | ||||
|     private val tocEntities: ArrayList<EditEntity> = ArrayList() | ||||
|     private val contentEntities: ArrayList<EditEntity> = ArrayList() | ||||
| 
 | ||||
|     private var mSoftKeyboardTool: PopupWindow? = null | ||||
|     private var mIsSoftKeyBoardShowing = false | ||||
| 
 | ||||
|     override fun onActivityCreated(savedInstanceState: Bundle?) { | ||||
|         initView() | ||||
|         viewModel.sourceLiveData.observe(this, Observer { | ||||
|             upRecyclerView(it) | ||||
|         }) | ||||
|         viewModel.initData(intent) | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         super.onDestroy() | ||||
|         mSoftKeyboardTool?.dismiss() | ||||
|     } | ||||
| 
 | ||||
|     override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { | ||||
|         menuInflater.inflate(R.menu.source_edit, menu) | ||||
|         return super.onCompatCreateOptionsMenu(menu) | ||||
|     } | ||||
| 
 | ||||
|     override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             R.id.menu_save -> { | ||||
|                 getSource()?.let { | ||||
|                     viewModel.save(it) { setResult(Activity.RESULT_OK); finish() } | ||||
|                 } | ||||
|             } | ||||
|             R.id.menu_debug_source -> { | ||||
|                 getSource()?.let { | ||||
|                     viewModel.save(it) { | ||||
|                         startActivity<BookSourceDebugActivity>(Pair("key", it.bookSourceUrl)) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             R.id.menu_copy_source -> { | ||||
|                 GSON.toJson(getSource())?.let { sourceStr -> | ||||
|                     val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? | ||||
|                     clipboard?.primaryClip = ClipData.newPlainText(null, sourceStr) | ||||
|                 } | ||||
|             } | ||||
|             R.id.menu_paste_source -> viewModel.pasteSource() | ||||
|         } | ||||
|         return super.onCompatOptionsItemSelected(item) | ||||
|     } | ||||
| 
 | ||||
|     private fun initView() { | ||||
|         ATH.applyEdgeEffectColor(recycler_view) | ||||
|         mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this) | ||||
|         window.decorView.viewTreeObserver.addOnGlobalLayoutListener(KeyboardOnGlobalChangeListener()) | ||||
|         recycler_view.layoutManager = LinearLayoutManager(this) | ||||
|         recycler_view.adapter = adapter | ||||
|         tab_layout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { | ||||
|             override fun onTabReselected(tab: TabLayout.Tab?) { | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|             override fun onTabUnselected(tab: TabLayout.Tab?) { | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|             override fun onTabSelected(tab: TabLayout.Tab?) { | ||||
|                 setEditEntities(tab?.position) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private fun setEditEntities(tabPosition: Int?) { | ||||
|         when (tabPosition) { | ||||
|             1 -> adapter.editEntities = searchEntities | ||||
|             2 -> adapter.editEntities = findEntities | ||||
|             3 -> adapter.editEntities = infoEntities | ||||
|             4 -> adapter.editEntities = tocEntities | ||||
|             5 -> adapter.editEntities = contentEntities | ||||
|             else -> adapter.editEntities = sourceEntities | ||||
|         } | ||||
|         recycler_view.scrollToPosition(0) | ||||
|     } | ||||
| 
 | ||||
|     private fun upRecyclerView(bookSource: BookSource?) { | ||||
|         bookSource?.let { | ||||
|             cb_is_enable.isChecked = it.enabled | ||||
|             cb_is_enable_find.isChecked = it.enabledExplore | ||||
|         } | ||||
|         //基本信息 | ||||
|         sourceEntities.clear() | ||||
|         sourceEntities.apply { | ||||
|             add(EditEntity("bookSourceUrl", bookSource?.bookSourceUrl, R.string.book_source_url)) | ||||
|             add(EditEntity("bookSourceName", bookSource?.bookSourceName, R.string.book_source_name)) | ||||
|             add( | ||||
|                 EditEntity( | ||||
|                     "bookSourceGroup", | ||||
|                     bookSource?.bookSourceGroup, | ||||
|                     R.string.book_source_group | ||||
|                 ) | ||||
|             ) | ||||
|             add(EditEntity("loginUrl", bookSource?.loginUrl, R.string.book_source_login_url)) | ||||
|             add(EditEntity("bookUrlPattern", bookSource?.bookUrlPattern, R.string.book_url_pattern)) | ||||
|             add(EditEntity("header", bookSource?.header, R.string.source_http_header)) | ||||
|         } | ||||
|         //搜索 | ||||
|         (bookSource?.getSearchRule()).let { searchRule -> | ||||
|             searchEntities.clear() | ||||
|             searchEntities.apply { | ||||
|                 add(EditEntity("searchUrl", bookSource?.searchUrl, R.string.rule_search_url)) | ||||
|                 add(EditEntity("bookList", searchRule?.bookList, R.string.rule_book_list)) | ||||
|                 add(EditEntity("name", searchRule?.name, R.string.rule_book_name)) | ||||
|                 add(EditEntity("author", searchRule?.author, R.string.rule_book_author)) | ||||
|                 add(EditEntity("kind", searchRule?.kind, R.string.rule_book_kind)) | ||||
|                 add(EditEntity("wordCount", searchRule?.wordCount, R.string.rule_word_count)) | ||||
|                 add(EditEntity("lastChapter", searchRule?.lastChapter, R.string.rule_last_chapter)) | ||||
|                 add(EditEntity("intro", searchRule?.intro, R.string.rule_book_intro)) | ||||
|                 add(EditEntity("coverUrl", searchRule?.coverUrl, R.string.rule_cover_url)) | ||||
|                 add(EditEntity("bookUrl", searchRule?.bookUrl, R.string.rule_book_url)) | ||||
|             } | ||||
|         } | ||||
|         //详情页 | ||||
|         (bookSource?.getBookInfoRule()).let { infoRule -> | ||||
|             infoEntities.clear() | ||||
|             infoEntities.apply { | ||||
|                 add(EditEntity("init", infoRule?.init, R.string.rule_book_info_init)) | ||||
|                 add(EditEntity("name", infoRule?.name, R.string.rule_book_name)) | ||||
|                 add(EditEntity("author", infoRule?.author, R.string.rule_book_author)) | ||||
|                 add(EditEntity("coverUrl", infoRule?.coverUrl, R.string.rule_cover_url)) | ||||
|                 add(EditEntity("intro", infoRule?.intro, R.string.rule_book_intro)) | ||||
|                 add(EditEntity("kind", infoRule?.kind, R.string.rule_book_kind)) | ||||
|                 add(EditEntity("wordCount", infoRule?.wordCount, R.string.rule_word_count)) | ||||
|                 add(EditEntity("lastChapter", infoRule?.lastChapter, R.string.rule_last_chapter)) | ||||
|                 add(EditEntity("tocUrl", infoRule?.tocUrl, R.string.rule_toc_url)) | ||||
|             } | ||||
|         } | ||||
|         //目录页 | ||||
|         (bookSource?.getTocRule()).let { tocRule -> | ||||
|             tocEntities.clear() | ||||
|             tocEntities.apply { | ||||
|                 add(EditEntity("chapterList", tocRule?.chapterList, R.string.rule_chapter_list)) | ||||
|                 add(EditEntity("chapterName", tocRule?.chapterName, R.string.rule_chapter_name)) | ||||
|                 add(EditEntity("chapterUrl", tocRule?.chapterUrl, R.string.rule_chapter_url)) | ||||
|                 add(EditEntity("nextTocUrl", tocRule?.nextTocUrl, R.string.rule_next_toc_url)) | ||||
|             } | ||||
|         } | ||||
|         //正文页 | ||||
|         (bookSource?.getContentRule()).let { contentRule -> | ||||
|             contentEntities.clear() | ||||
|             contentEntities.apply { | ||||
|                 add(EditEntity("content", contentRule?.content, R.string.rule_book_content)) | ||||
|                 add( | ||||
|                     EditEntity( | ||||
|                         "nextContentUrl", | ||||
|                         contentRule?.nextContentUrl, | ||||
|                         R.string.rule_content_url_next | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         //发现 | ||||
|         (bookSource?.getExploreRule()).let { exploreRule -> | ||||
|             findEntities.clear() | ||||
|             findEntities.apply { | ||||
|                 add(EditEntity("exploreUrl", bookSource?.exploreUrl, R.string.rule_find_url)) | ||||
|                 add(EditEntity("bookList", exploreRule?.bookList, R.string.rule_book_list)) | ||||
|                 add(EditEntity("name", exploreRule?.name, R.string.rule_book_name)) | ||||
|                 add(EditEntity("author", exploreRule?.author, R.string.rule_book_author)) | ||||
|                 add(EditEntity("kind", exploreRule?.kind, R.string.rule_book_kind)) | ||||
|                 add(EditEntity("wordCount", exploreRule?.wordCount, R.string.rule_word_count)) | ||||
|                 add(EditEntity("intro", exploreRule?.intro, R.string.rule_book_intro)) | ||||
|                 add(EditEntity("lastChapter", exploreRule?.lastChapter, R.string.rule_last_chapter)) | ||||
|                 add(EditEntity("coverUrl", exploreRule?.coverUrl, R.string.rule_cover_url)) | ||||
|                 add(EditEntity("bookUrl", exploreRule?.bookUrl, R.string.rule_book_url)) | ||||
|             } | ||||
|         } | ||||
|         setEditEntities(0) | ||||
|     } | ||||
| 
 | ||||
|     private fun getSource(): BookSource? { | ||||
|         val source = viewModel.sourceLiveData.value ?: BookSource() | ||||
|         source.enabled = cb_is_enable.isChecked | ||||
|         source.enabledExplore = cb_is_enable_find.isChecked | ||||
|         viewModel.sourceLiveData.value?.let { | ||||
|             source.customOrder = it.customOrder | ||||
|             source.weight = it.weight | ||||
|         } | ||||
|         val searchRule = SearchRule() | ||||
|         val exploreRule = ExploreRule() | ||||
|         val bookInfoRule = BookInfoRule() | ||||
|         val tocRule = TocRule() | ||||
|         val contentRule = ContentRule() | ||||
|         sourceEntities.forEach { | ||||
|             when (it.key) { | ||||
|                 "bookSourceUrl" -> source.bookSourceUrl = it.value ?: "" | ||||
|                 "bookSourceName" -> source.bookSourceName = it.value ?: "" | ||||
|                 "bookSourceGroup" -> source.bookSourceGroup = it.value | ||||
|                 "loginUrl" -> source.loginUrl = it.value | ||||
|                 "bookUrlPattern" -> source.bookUrlPattern = it.value | ||||
|                 "header" -> source.header = it.value | ||||
|             } | ||||
|         } | ||||
|         if (source.bookSourceUrl.isBlank() || source.bookSourceName.isBlank()) { | ||||
|             toast("书源名称和URL不能为空") | ||||
|             return null | ||||
|         } | ||||
|         searchEntities.forEach { | ||||
|             when (it.key) { | ||||
|                 "searchUrl" -> source.searchUrl = it.value | ||||
|                 "bookList" -> searchRule.bookList = it.value | ||||
|                 "name" -> searchRule.name = it.value | ||||
|                 "author" -> searchRule.author = it.value | ||||
|                 "kind" -> searchRule.kind = it.value | ||||
|                 "intro" -> searchRule.intro = it.value | ||||
|                 "updateTime" -> searchRule.updateTime = it.value | ||||
|                 "wordCount" -> searchRule.wordCount = it.value | ||||
|                 "lastChapter" -> searchRule.lastChapter = it.value | ||||
|                 "coverUrl" -> searchRule.coverUrl = it.value | ||||
|                 "bookUrl" -> searchRule.bookUrl = it.value | ||||
|             } | ||||
|         } | ||||
|         findEntities.forEach { | ||||
|             when (it.key) { | ||||
|                 "exploreUrl" -> source.exploreUrl = it.value | ||||
|                 "bookList" -> exploreRule.bookList = it.value | ||||
|                 "name" -> exploreRule.name = it.value | ||||
|                 "author" -> exploreRule.author = it.value | ||||
|                 "kind" -> exploreRule.kind = it.value | ||||
|                 "intro" -> exploreRule.intro = it.value | ||||
|                 "updateTime" -> exploreRule.updateTime = it.value | ||||
|                 "wordCount" -> exploreRule.wordCount = it.value | ||||
|                 "lastChapter" -> exploreRule.lastChapter = it.value | ||||
|                 "coverUrl" -> exploreRule.coverUrl = it.value | ||||
|                 "bookUrl" -> exploreRule.bookUrl = it.value | ||||
|             } | ||||
|         } | ||||
|         infoEntities.forEach { | ||||
|             when (it.key) { | ||||
|                 "init" -> bookInfoRule.init = it.value | ||||
|                 "name" -> bookInfoRule.name = it.value | ||||
|                 "author" -> bookInfoRule.author = it.value | ||||
|                 "kind" -> bookInfoRule.kind = it.value | ||||
|                 "intro" -> bookInfoRule.intro = it.value | ||||
|                 "updateTime" -> bookInfoRule.updateTime = it.value | ||||
|                 "wordCount" -> bookInfoRule.wordCount = it.value | ||||
|                 "lastChapter" -> bookInfoRule.lastChapter = it.value | ||||
|                 "coverUrl" -> bookInfoRule.coverUrl = it.value | ||||
|                 "tocUrl" -> bookInfoRule.tocUrl = it.value | ||||
|             } | ||||
|         } | ||||
|         tocEntities.forEach { | ||||
|             when (it.key) { | ||||
|                 "chapterList" -> tocRule.chapterList = it.value | ||||
|                 "chapterName" -> tocRule.chapterName = it.value | ||||
|                 "chapterUrl" -> tocRule.chapterUrl = it.value | ||||
|                 "nextTocUrl" -> tocRule.nextTocUrl = it.value | ||||
|             } | ||||
|         } | ||||
|         contentEntities.forEach { | ||||
|             when (it.key) { | ||||
|                 "content" -> contentRule.content = it.value | ||||
|                 "nextContentUrl" -> contentRule.nextContentUrl = it.value | ||||
|             } | ||||
|         } | ||||
|         source.ruleSearch = GSON.toJson(searchRule) | ||||
|         source.ruleExplore = GSON.toJson(exploreRule) | ||||
|         source.ruleBookInfo = GSON.toJson(bookInfoRule) | ||||
|         source.ruleToc = GSON.toJson(tocRule) | ||||
|         source.ruleContent = GSON.toJson(contentRule) | ||||
|         return source | ||||
|     } | ||||
| 
 | ||||
|     override fun sendText(text: String) { | ||||
|         if (text.isBlank()) return | ||||
|         val view = window.decorView.findFocus() | ||||
|         if (view is EditText) { | ||||
|             val start = view.selectionStart | ||||
|             val end = view.selectionEnd | ||||
|             val edit = view.editableText//获取EditText的文字 | ||||
|             if (start < 0 || start >= edit.length) { | ||||
|                 edit.append(text) | ||||
|             } else { | ||||
|                 edit.replace(start, end, text)//光标所在位置插入文字 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun showKeyboardTopPopupWindow() { | ||||
|         mSoftKeyboardTool?.isShowing?.let { if (it) return } | ||||
|         if (!isFinishing) { | ||||
|             mSoftKeyboardTool?.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun closePopupWindow() { | ||||
|         mSoftKeyboardTool?.let { | ||||
|             if (it.isShowing) { | ||||
|                 it.dismiss() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener { | ||||
|         override fun onGlobalLayout() { | ||||
|             val rect = Rect() | ||||
|             // 获取当前页面窗口的显示范围 | ||||
|             window.decorView.getWindowVisibleDisplayFrame(rect) | ||||
|             val screenHeight = this@BookSourceEditActivity.displayMetrics.heightPixels | ||||
|             val keyboardHeight = screenHeight - rect.bottom // 输入法的高度 | ||||
|             val preShowing = mIsSoftKeyBoardShowing | ||||
|             if (abs(keyboardHeight) > screenHeight / 5) { | ||||
|                 mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法 | ||||
|                 recycler_view.setPadding(0, 0, 0, 100) | ||||
|                 showKeyboardTopPopupWindow() | ||||
|             } else { | ||||
|                 mIsSoftKeyBoardShowing = false | ||||
|                 recycler_view.setPadding(0, 0, 0, 0) | ||||
|                 if (preShowing) { | ||||
|                     closePopupWindow() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -1,622 +0,0 @@ | ||||
| package io.legado.app.ui.book.source.edit | ||||
| 
 | ||||
| import android.app.Activity | ||||
| import android.content.ClipData | ||||
| import android.content.ClipboardManager | ||||
| import android.content.Context | ||||
| import android.graphics.Rect | ||||
| import android.os.Bundle | ||||
| import android.view.Gravity | ||||
| import android.view.Menu | ||||
| import android.view.MenuItem | ||||
| import android.view.ViewTreeObserver | ||||
| import android.widget.EditText | ||||
| import android.widget.PopupWindow | ||||
| import androidx.lifecycle.Observer | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import com.google.android.material.tabs.TabLayout | ||||
| import io.legado.app.R | ||||
| import io.legado.app.base.VMBaseActivity | ||||
| import io.legado.app.constant.AppConst | ||||
| import io.legado.app.data.entities.BookSource | ||||
| import io.legado.app.data.entities.rule.* | ||||
| import io.legado.app.lib.theme.ATH | ||||
| import io.legado.app.ui.book.source.debug.SourceDebugActivity | ||||
| import io.legado.app.ui.widget.KeyboardToolPop | ||||
| import io.legado.app.utils.GSON | ||||
| import io.legado.app.utils.getViewModel | ||||
| import kotlinx.android.synthetic.main.activity_book_source_edit.* | ||||
| import org.jetbrains.anko.displayMetrics | ||||
| import org.jetbrains.anko.startActivity | ||||
| import org.jetbrains.anko.toast | ||||
| import kotlin.math.abs | ||||
| 
 | ||||
| class SourceEditActivity : | ||||
|     VMBaseActivity<SourceEditViewModel>(R.layout.activity_book_source_edit, false), | ||||
|     KeyboardToolPop.OnClickListener { | ||||
|     override val viewModel: SourceEditViewModel | ||||
|         get() = getViewModel(SourceEditViewModel::class.java) | ||||
| 
 | ||||
|     private val adapter = SourceEditAdapter() | ||||
|     private val sourceEntities: ArrayList<EditEntity> = ArrayList() | ||||
|     private val searchEntities: ArrayList<EditEntity> = ArrayList() | ||||
|     private val findEntities: ArrayList<EditEntity> = ArrayList() | ||||
|     private val infoEntities: ArrayList<EditEntity> = ArrayList() | ||||
|     private val tocEntities: ArrayList<EditEntity> = ArrayList() | ||||
|     private val contentEntities: ArrayList<EditEntity> = ArrayList() | ||||
| 
 | ||||
|     private var mSoftKeyboardTool: PopupWindow? = null | ||||
|     private var mIsSoftKeyBoardShowing = false | ||||
| 
 | ||||
|     override fun onActivityCreated(savedInstanceState: Bundle?) { | ||||
|         initView() | ||||
|         viewModel.sourceLiveData.observe(this, Observer { | ||||
|             upRecyclerView(it) | ||||
|         }) | ||||
|         if (viewModel.sourceLiveData.value == null) { | ||||
|             val sourceID = intent.getStringExtra("data") | ||||
|             if (sourceID == null) { | ||||
|                 upRecyclerView(null) | ||||
|             } else { | ||||
|                 sourceID.let { viewModel.setBookSource(sourceID) } | ||||
|             } | ||||
|         } else { | ||||
|             upRecyclerView(viewModel.sourceLiveData.value) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         super.onDestroy() | ||||
|         mSoftKeyboardTool?.dismiss() | ||||
|     } | ||||
| 
 | ||||
|     override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { | ||||
|         menuInflater.inflate(R.menu.source_edit, menu) | ||||
|         return super.onCompatCreateOptionsMenu(menu) | ||||
|     } | ||||
| 
 | ||||
|     override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             R.id.menu_save -> { | ||||
|                 val bookSource = getSource() | ||||
|                 if (bookSource == null) { | ||||
|                     toast("书源名称和URL不能为空") | ||||
|                 } else { | ||||
|                     viewModel.save(bookSource) { setResult(Activity.RESULT_OK); finish() } | ||||
|                 } | ||||
|             } | ||||
|             R.id.menu_debug_source -> { | ||||
|                 val bookSource = getSource() | ||||
|                 if (bookSource == null) { | ||||
|                     toast("书源名称和URL不能为空") | ||||
|                 } else { | ||||
|                     viewModel.save(bookSource) { | ||||
|                         startActivity<SourceDebugActivity>("key" to bookSource.bookSourceUrl) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             R.id.menu_copy_source -> { | ||||
|                 GSON.toJson(getSource())?.let { sourceStr -> | ||||
|                     val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? | ||||
|                     clipboard?.primaryClip = ClipData.newPlainText(null, sourceStr) | ||||
|                 } | ||||
|             } | ||||
|             R.id.menu_paste_source -> viewModel.pasteSource() | ||||
|         } | ||||
|         return super.onCompatOptionsItemSelected(item) | ||||
|     } | ||||
| 
 | ||||
|     private fun initView() { | ||||
|         ATH.applyEdgeEffectColor(recycler_view) | ||||
|         mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this) | ||||
|         window.decorView.viewTreeObserver.addOnGlobalLayoutListener(KeyboardOnGlobalChangeListener()) | ||||
|         recycler_view.layoutManager = LinearLayoutManager(this) | ||||
|         recycler_view.adapter = adapter | ||||
|         tab_layout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { | ||||
|             override fun onTabReselected(tab: TabLayout.Tab?) { | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|             override fun onTabUnselected(tab: TabLayout.Tab?) { | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|             override fun onTabSelected(tab: TabLayout.Tab?) { | ||||
|                 setEditEntities(tab?.position) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private fun setEditEntities(tabPosition: Int?) { | ||||
|         when (tabPosition) { | ||||
|             1 -> adapter.editEntities = searchEntities | ||||
|             2 -> adapter.editEntities = findEntities | ||||
|             3 -> adapter.editEntities = infoEntities | ||||
|             4 -> adapter.editEntities = tocEntities | ||||
|             5 -> adapter.editEntities = contentEntities | ||||
|             else -> adapter.editEntities = sourceEntities | ||||
|         } | ||||
|         recycler_view.scrollToPosition(0) | ||||
|     } | ||||
| 
 | ||||
|     private fun upRecyclerView(bookSource: BookSource?) { | ||||
|         bookSource?.let { | ||||
|             cb_is_enable.isChecked = it.enabled | ||||
|             cb_is_enable_find.isChecked = it.enabledExplore | ||||
|         } | ||||
|         //基本信息 | ||||
|         with(bookSource) { | ||||
|             sourceEntities.clear() | ||||
|             sourceEntities | ||||
|                 .add( | ||||
|                     EditEntity( | ||||
|                         "bookSourceUrl", | ||||
|                         this?.bookSourceUrl, | ||||
|                         R.string.book_source_url | ||||
|                     ) | ||||
|                 ) | ||||
|             sourceEntities | ||||
|                 .add( | ||||
|                     EditEntity( | ||||
|                         "bookSourceName", | ||||
|                         this?.bookSourceName, | ||||
|                         R.string.book_source_name | ||||
|                     ) | ||||
|                 ) | ||||
|             sourceEntities.add( | ||||
|                 EditEntity( | ||||
|                     "bookSourceGroup", | ||||
|                     this?.bookSourceGroup, | ||||
|                     R.string.book_source_group | ||||
|                 ) | ||||
|             ) | ||||
|             sourceEntities | ||||
|                 .add( | ||||
|                     EditEntity( | ||||
|                         "loginUrl", | ||||
|                         this?.loginUrl, | ||||
|                         R.string.book_source_login_url | ||||
|                     ) | ||||
|                 ) | ||||
|             sourceEntities | ||||
|                 .add( | ||||
|                     EditEntity( | ||||
|                         "bookUrlPattern", | ||||
|                         this?.bookUrlPattern, | ||||
|                         R.string.book_url_pattern | ||||
|                     ) | ||||
|                 ) | ||||
|             sourceEntities.add( | ||||
|                 EditEntity( | ||||
|                     "header", | ||||
|                     this?.header, | ||||
|                     R.string.source_http_header | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
|         //搜索 | ||||
|         with(bookSource?.getSearchRule()) { | ||||
|             searchEntities.clear() | ||||
|             searchEntities | ||||
|                 .add( | ||||
|                     EditEntity( | ||||
|                         "searchUrl", | ||||
|                         bookSource?.searchUrl, | ||||
|                         R.string.rule_search_url | ||||
|                     ) | ||||
|                 ) | ||||
|             searchEntities.add( | ||||
|                 EditEntity( | ||||
|                     "bookList", | ||||
|                     this?.bookList, | ||||
|                     R.string.rule_book_list | ||||
|                 ) | ||||
|             ) | ||||
|             searchEntities.add( | ||||
|                 EditEntity( | ||||
|                     "name", | ||||
|                     this?.name, | ||||
|                     R.string.rule_book_name | ||||
|                 ) | ||||
|             ) | ||||
|             searchEntities.add( | ||||
|                 EditEntity( | ||||
|                     "author", | ||||
|                     this?.author, | ||||
|                     R.string.rule_book_author | ||||
|                 ) | ||||
|             ) | ||||
|             searchEntities.add( | ||||
|                 EditEntity( | ||||
|                     "kind", | ||||
|                     this?.kind, | ||||
|                     R.string.rule_book_kind | ||||
|                 ) | ||||
|             ) | ||||
|             searchEntities.add( | ||||
|                 EditEntity( | ||||
|                     "wordCount", | ||||
|                     this?.wordCount, | ||||
|                     R.string.rule_word_count | ||||
|                 ) | ||||
|             ) | ||||
|             searchEntities | ||||
|                 .add( | ||||
|                     EditEntity( | ||||
|                         "lastChapter", | ||||
|                         this?.lastChapter, | ||||
|                         R.string.rule_last_chapter | ||||
|                     ) | ||||
|                 ) | ||||
|             searchEntities.add( | ||||
|                 EditEntity( | ||||
|                     "intro", | ||||
|                     this?.intro, | ||||
|                     R.string.rule_book_intro | ||||
|                 ) | ||||
|             ) | ||||
|             searchEntities.add( | ||||
|                 EditEntity( | ||||
|                     "coverUrl", | ||||
|                     this?.coverUrl, | ||||
|                     R.string.rule_cover_url | ||||
|                 ) | ||||
|             ) | ||||
|             searchEntities.add( | ||||
|                 EditEntity( | ||||
|                     "bookUrl", | ||||
|                     this?.bookUrl, | ||||
|                     R.string.rule_book_url | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
|         //详情页 | ||||
|         with(bookSource?.getBookInfoRule()) { | ||||
|             infoEntities.clear() | ||||
|             infoEntities.add( | ||||
|                 EditEntity( | ||||
|                     "init", | ||||
|                     this?.init, | ||||
|                     R.string.rule_book_info_init | ||||
|                 ) | ||||
|             ) | ||||
|             infoEntities.add( | ||||
|                 EditEntity( | ||||
|                     "name", | ||||
|                     this?.name, | ||||
|                     R.string.rule_book_name | ||||
|                 ) | ||||
|             ) | ||||
|             infoEntities.add( | ||||
|                 EditEntity( | ||||
|                     "author", | ||||
|                     this?.author, | ||||
|                     R.string.rule_book_author | ||||
|                 ) | ||||
|             ) | ||||
|             infoEntities.add( | ||||
|                 EditEntity( | ||||
|                     "coverUrl", | ||||
|                     this?.coverUrl, | ||||
|                     R.string.rule_cover_url | ||||
|                 ) | ||||
|             ) | ||||
|             infoEntities.add( | ||||
|                 EditEntity( | ||||
|                     "intro", | ||||
|                     this?.intro, | ||||
|                     R.string.rule_book_intro | ||||
|                 ) | ||||
|             ) | ||||
|             infoEntities.add( | ||||
|                 EditEntity( | ||||
|                     "kind", | ||||
|                     this?.kind, | ||||
|                     R.string.rule_book_kind | ||||
|                 ) | ||||
|             ) | ||||
|             infoEntities.add( | ||||
|                 EditEntity( | ||||
|                     "wordCount", | ||||
|                     this?.wordCount, | ||||
|                     R.string.rule_word_count | ||||
|                 ) | ||||
|             ) | ||||
|             infoEntities.add( | ||||
|                 EditEntity( | ||||
|                     "lastChapter", | ||||
|                     this?.lastChapter, | ||||
|                     R.string.rule_last_chapter | ||||
|                 ) | ||||
|             ) | ||||
|             infoEntities.add( | ||||
|                 EditEntity( | ||||
|                     "tocUrl", | ||||
|                     this?.tocUrl, | ||||
|                     R.string.rule_toc_url | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
|         //目录页 | ||||
|         with(bookSource?.getTocRule()) { | ||||
|             tocEntities.clear() | ||||
|             tocEntities.add( | ||||
|                 EditEntity( | ||||
|                     "chapterList", | ||||
|                     this?.chapterList, | ||||
|                     R.string.rule_chapter_list | ||||
|                 ) | ||||
|             ) | ||||
|             tocEntities.add( | ||||
|                 EditEntity( | ||||
|                     "chapterName", | ||||
|                     this?.chapterName, | ||||
|                     R.string.rule_chapter_name | ||||
|                 ) | ||||
|             ) | ||||
|             tocEntities.add( | ||||
|                 EditEntity( | ||||
|                     "chapterUrl", | ||||
|                     this?.chapterUrl, | ||||
|                     R.string.rule_chapter_url | ||||
|                 ) | ||||
|             ) | ||||
|             tocEntities.add( | ||||
|                 EditEntity( | ||||
|                     "nextTocUrl", | ||||
|                     this?.nextTocUrl, | ||||
|                     R.string.rule_next_toc_url | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
|         //正文页 | ||||
|         with(bookSource?.getContentRule()) { | ||||
|             contentEntities.clear() | ||||
|             contentEntities.add( | ||||
|                 EditEntity( | ||||
|                     "content", | ||||
|                     this?.content, | ||||
|                     R.string.rule_book_content | ||||
|                 ) | ||||
|             ) | ||||
|             contentEntities.add( | ||||
|                 EditEntity( | ||||
|                     "nextContentUrl", | ||||
|                     this?.nextContentUrl, | ||||
|                     R.string.rule_content_url_next | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         //发现 | ||||
|         with(bookSource?.getExploreRule()) { | ||||
|             findEntities.clear() | ||||
|             findEntities.add( | ||||
|                 EditEntity( | ||||
|                     "exploreUrl", | ||||
|                     bookSource?.exploreUrl, | ||||
|                     R.string.rule_find_url | ||||
|                 ) | ||||
|             ) | ||||
|             findEntities.add( | ||||
|                 EditEntity( | ||||
|                     "bookList", | ||||
|                     this?.bookList, | ||||
|                     R.string.rule_book_list | ||||
|                 ) | ||||
|             ) | ||||
|             findEntities.add( | ||||
|                 EditEntity( | ||||
|                     "name", | ||||
|                     this?.name, | ||||
|                     R.string.rule_book_name | ||||
|                 ) | ||||
|             ) | ||||
|             findEntities.add( | ||||
|                 EditEntity( | ||||
|                     "author", | ||||
|                     this?.author, | ||||
|                     R.string.rule_book_author | ||||
|                 ) | ||||
|             ) | ||||
|             findEntities.add( | ||||
|                 EditEntity( | ||||
|                     "kind", | ||||
|                     this?.kind, | ||||
|                     R.string.rule_book_kind | ||||
|                 ) | ||||
|             ) | ||||
|             findEntities.add( | ||||
|                 EditEntity( | ||||
|                     "wordCount", | ||||
|                     this?.wordCount, | ||||
|                     R.string.rule_word_count | ||||
|                 ) | ||||
|             ) | ||||
|             findEntities.add( | ||||
|                 EditEntity( | ||||
|                     "intro", | ||||
|                     this?.intro, | ||||
|                     R.string.rule_book_intro | ||||
|                 ) | ||||
|             ) | ||||
|             findEntities.add( | ||||
|                 EditEntity( | ||||
|                     "lastChapter", | ||||
|                     this?.lastChapter, | ||||
|                     R.string.rule_last_chapter | ||||
|                 ) | ||||
|             ) | ||||
|             findEntities.add( | ||||
|                 EditEntity( | ||||
|                     "coverUrl", | ||||
|                     this?.coverUrl, | ||||
|                     R.string.rule_cover_url | ||||
|                 ) | ||||
|             ) | ||||
|             findEntities.add( | ||||
|                 EditEntity( | ||||
|                     "bookUrl", | ||||
|                     this?.bookUrl, | ||||
|                     R.string.rule_book_url | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
|         setEditEntities(0) | ||||
|     } | ||||
| 
 | ||||
|     private fun getSource(): BookSource? { | ||||
|         val source = viewModel.sourceLiveData.value ?: BookSource() | ||||
|         source.enabled = cb_is_enable.isChecked | ||||
|         source.enabledExplore = cb_is_enable_find.isChecked | ||||
|         viewModel.sourceLiveData.value?.let { | ||||
|             source.customOrder = it.customOrder | ||||
|             source.weight = it.weight | ||||
|         } | ||||
|         val searchRule = SearchRule() | ||||
|         val exploreRule = ExploreRule() | ||||
|         val bookInfoRule = BookInfoRule() | ||||
|         val tocRule = TocRule() | ||||
|         val contentRule = ContentRule() | ||||
|         for (entity in sourceEntities) { | ||||
|             with(entity) { | ||||
|                 when (key) { | ||||
|                     "bookSourceUrl" -> value?.let { source.bookSourceUrl = it } ?: return null | ||||
|                     "bookSourceName" -> value?.let { source.bookSourceName = it } ?: return null | ||||
|                     "bookSourceGroup" -> source.bookSourceGroup = value | ||||
|                     "loginUrl" -> source.loginUrl = value | ||||
|                     "bookUrlPattern" -> source.bookUrlPattern = value | ||||
|                     "header" -> source.header = value | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         for (entity in searchEntities) { | ||||
|             with(entity) { | ||||
|                 when (key) { | ||||
|                     "searchUrl" -> source.searchUrl = value | ||||
|                     "bookList" -> searchRule.bookList = value | ||||
|                     "name" -> searchRule.name = value | ||||
|                     "author" -> searchRule.author = value | ||||
|                     "kind" -> searchRule.kind = value | ||||
|                     "intro" -> searchRule.intro = value | ||||
|                     "updateTime" -> searchRule.updateTime = value | ||||
|                     "wordCount" -> searchRule.wordCount = value | ||||
|                     "lastChapter" -> searchRule.lastChapter = value | ||||
|                     "coverUrl" -> searchRule.coverUrl = value | ||||
|                     "bookUrl" -> searchRule.bookUrl = value | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         for (entity in findEntities) { | ||||
|             with(entity) { | ||||
|                 when (key) { | ||||
|                     "exploreUrl" -> source.exploreUrl = value | ||||
|                     "bookList" -> exploreRule.bookList = value | ||||
|                     "name" -> exploreRule.name = value | ||||
|                     "author" -> exploreRule.author = value | ||||
|                     "kind" -> exploreRule.kind = value | ||||
|                     "intro" -> exploreRule.intro = value | ||||
|                     "updateTime" -> exploreRule.updateTime = value | ||||
|                     "wordCount" -> exploreRule.wordCount = value | ||||
|                     "lastChapter" -> exploreRule.lastChapter = value | ||||
|                     "coverUrl" -> exploreRule.coverUrl = value | ||||
|                     "bookUrl" -> exploreRule.bookUrl = value | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         for (entity in infoEntities) { | ||||
|             with(entity) { | ||||
|                 when (key) { | ||||
|                     "init" -> bookInfoRule.init = value | ||||
|                     "name" -> bookInfoRule.name = value | ||||
|                     "author" -> bookInfoRule.author = value | ||||
|                     "kind" -> bookInfoRule.kind = value | ||||
|                     "intro" -> bookInfoRule.intro = value | ||||
|                     "updateTime" -> bookInfoRule.updateTime = value | ||||
|                     "wordCount" -> bookInfoRule.wordCount = value | ||||
|                     "lastChapter" -> bookInfoRule.lastChapter = value | ||||
|                     "coverUrl" -> bookInfoRule.coverUrl = value | ||||
|                     "tocUrl" -> bookInfoRule.tocUrl = value | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         for (entity in tocEntities) { | ||||
|             with(entity) { | ||||
|                 when (key) { | ||||
|                     "chapterList" -> tocRule.chapterList = value | ||||
|                     "chapterName" -> tocRule.chapterName = value | ||||
|                     "chapterUrl" -> tocRule.chapterUrl = value | ||||
|                     "nextTocUrl" -> tocRule.nextTocUrl = value | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         for (entity in contentEntities) { | ||||
|             with(entity) { | ||||
|                 when (key) { | ||||
|                     "content" -> contentRule.content = value | ||||
|                     "nextContentUrl" -> contentRule.nextContentUrl = value | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         source.ruleSearch = GSON.toJson(searchRule) | ||||
|         source.ruleExplore = GSON.toJson(exploreRule) | ||||
|         source.ruleBookInfo = GSON.toJson(bookInfoRule) | ||||
|         source.ruleToc = GSON.toJson(tocRule) | ||||
|         source.ruleContent = GSON.toJson(contentRule) | ||||
|         return source | ||||
|     } | ||||
| 
 | ||||
|     override fun click(text: String) { | ||||
|         if (text.isBlank()) return | ||||
|         val view = window.decorView.findFocus() | ||||
|         if (view is EditText) { | ||||
|             val start = view.selectionStart | ||||
|             val end = view.selectionEnd | ||||
|             val edit = view.editableText//获取EditText的文字 | ||||
|             if (start < 0 || start >= edit.length) { | ||||
|                 edit.append(text) | ||||
|             } else { | ||||
|                 edit.replace(start, end, text)//光标所在位置插入文字 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun showKeyboardTopPopupWindow() { | ||||
|         mSoftKeyboardTool?.isShowing?.let { if (it) return } | ||||
|         if (!isFinishing) { | ||||
|             mSoftKeyboardTool?.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun closePopupWindow() { | ||||
|         mSoftKeyboardTool?.let { | ||||
|             if (it.isShowing) { | ||||
|                 it.dismiss() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener { | ||||
|         override fun onGlobalLayout() { | ||||
|             val rect = Rect() | ||||
|             // 获取当前页面窗口的显示范围 | ||||
|             window.decorView.getWindowVisibleDisplayFrame(rect) | ||||
|             val screenHeight = this@SourceEditActivity.displayMetrics.heightPixels | ||||
|             val keyboardHeight = screenHeight - rect.bottom // 输入法的高度 | ||||
|             val preShowing = mIsSoftKeyBoardShowing | ||||
|             if (abs(keyboardHeight) > screenHeight / 5) { | ||||
|                 mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法 | ||||
|                 recycler_view.setPadding(0, 0, 0, 100) | ||||
|                 showKeyboardTopPopupWindow() | ||||
|             } else { | ||||
|                 mIsSoftKeyBoardShowing = false | ||||
|                 recycler_view.setPadding(0, 0, 0, 0) | ||||
|                 if (preShowing) { | ||||
|                     closePopupWindow() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class EditEntity(var key: String, var value: String?, var hint: Int) | ||||
| } | ||||
| @ -0,0 +1,176 @@ | ||||
| package io.legado.app.ui.main.bookshelf | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.os.Bundle | ||||
| import android.util.DisplayMetrics | ||||
| import android.view.LayoutInflater | ||||
| import android.view.MenuItem | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.EditText | ||||
| import androidx.appcompat.widget.Toolbar | ||||
| import androidx.fragment.app.DialogFragment | ||||
| import androidx.lifecycle.Observer | ||||
| import androidx.recyclerview.widget.DividerItemDecoration | ||||
| import androidx.recyclerview.widget.ItemTouchHelper | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import io.legado.app.App | ||||
| import io.legado.app.R | ||||
| import io.legado.app.base.adapter.ItemViewHolder | ||||
| import io.legado.app.base.adapter.SimpleRecyclerAdapter | ||||
| import io.legado.app.data.entities.BookGroup | ||||
| import io.legado.app.help.ItemTouchCallback | ||||
| import io.legado.app.lib.dialogs.alert | ||||
| import io.legado.app.lib.dialogs.customView | ||||
| import io.legado.app.lib.dialogs.noButton | ||||
| import io.legado.app.lib.dialogs.yesButton | ||||
| import io.legado.app.utils.* | ||||
| import kotlinx.android.synthetic.main.dialog_edit_text.view.* | ||||
| import kotlinx.android.synthetic.main.dialog_recycler_view.* | ||||
| import kotlinx.android.synthetic.main.item_group_manage.view.* | ||||
| import org.jetbrains.anko.sdk27.listeners.onClick | ||||
| 
 | ||||
| class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { | ||||
|     private lateinit var viewModel: BookshelfViewModel | ||||
|     private lateinit var adapter: GroupAdapter | ||||
|     private var callBack: CallBack? = null | ||||
| 
 | ||||
|     override fun onStart() { | ||||
|         super.onStart() | ||||
|         val dm = DisplayMetrics() | ||||
|         activity?.windowManager?.defaultDisplay?.getMetrics(dm) | ||||
|         dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
|         container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View? { | ||||
|         viewModel = getViewModel(BookshelfViewModel::class.java) | ||||
|         return inflater.inflate(R.layout.dialog_recycler_view, container) | ||||
|     } | ||||
| 
 | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         callBack = parentFragment as? CallBack | ||||
|         initData() | ||||
|     } | ||||
| 
 | ||||
|     private fun initData() { | ||||
|         tool_bar.title = getString(R.string.group_manage) | ||||
|         tool_bar.inflateMenu(R.menu.book_group_manage) | ||||
|         tool_bar.menu.applyTint(requireContext(), false) | ||||
|         tool_bar.setOnMenuItemClickListener(this) | ||||
|         tool_bar.menu.findItem(R.id.menu_group_local) | ||||
|             .isChecked = getPrefBoolean("bookGroupLocal", true) | ||||
|         tool_bar.menu.findItem(R.id.menu_group_audio) | ||||
|             .isChecked = getPrefBoolean("bookGroupAudio", true) | ||||
|         adapter = GroupAdapter(requireContext()) | ||||
|         recycler_view.layoutManager = LinearLayoutManager(requireContext()) | ||||
|         recycler_view.addItemDecoration( | ||||
|             DividerItemDecoration(requireContext(), RecyclerView.VERTICAL) | ||||
|         ) | ||||
|         recycler_view.adapter = adapter | ||||
|         App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer { | ||||
|             adapter.setItems(it) | ||||
|         }) | ||||
|         val itemTouchCallback = ItemTouchCallback() | ||||
|         itemTouchCallback.onItemTouchCallbackListener = adapter | ||||
|         itemTouchCallback.isCanDrag = true | ||||
|         ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) | ||||
|     } | ||||
| 
 | ||||
|     override fun onMenuItemClick(item: MenuItem?): Boolean { | ||||
|         when (item?.itemId) { | ||||
|             R.id.menu_add -> addGroup() | ||||
|             R.id.menu_group_local -> { | ||||
|                 item.isChecked = !item.isChecked | ||||
|                 putPrefBoolean("bookGroupLocal", item.isChecked) | ||||
|                 callBack?.upGroup() | ||||
|             } | ||||
|             R.id.menu_group_audio -> { | ||||
|                 item.isChecked = !item.isChecked | ||||
|                 putPrefBoolean("bookGroupAudio", item.isChecked) | ||||
|                 callBack?.upGroup() | ||||
|             } | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("InflateParams") | ||||
|     private fun addGroup() { | ||||
|         alert(title = getString(R.string.add_group)) { | ||||
|             var editText: EditText? = null | ||||
|             customView { | ||||
|                 layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { | ||||
|                     editText = edit_view.apply { | ||||
|                         hint = "分组名称" | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             yesButton { | ||||
|                 editText?.text?.toString()?.let { | ||||
|                     if (it.isNotBlank()) { | ||||
|                         viewModel.addGroup(it) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             noButton() | ||||
|         }.show().applyTint().requestInputMethod() | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("InflateParams") | ||||
|     private fun editGroup(bookGroup: BookGroup) { | ||||
|         alert(title = getString(R.string.group_edit)) { | ||||
|             var editText: EditText? = null | ||||
|             customView { | ||||
|                 layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { | ||||
|                     editText = edit_view.apply { | ||||
|                         hint = "分组名称" | ||||
|                         setText(bookGroup.groupName) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             yesButton { | ||||
|                 viewModel.upGroup(bookGroup.copy(groupName = editText?.text?.toString() ?: "")) | ||||
|             } | ||||
|             noButton() | ||||
|         }.show().applyTint().requestInputMethod() | ||||
|     } | ||||
| 
 | ||||
|     private inner class GroupAdapter(context: Context) : | ||||
|         SimpleRecyclerAdapter<BookGroup>(context, R.layout.item_group_manage), | ||||
|         ItemTouchCallback.OnItemTouchCallbackListener { | ||||
| 
 | ||||
|         override fun convert(holder: ItemViewHolder, item: BookGroup, payloads: MutableList<Any>) { | ||||
|             with(holder.itemView) { | ||||
|                 tv_group.text = item.groupName | ||||
|                 tv_edit.onClick { editGroup(item) } | ||||
|                 tv_del.onClick { viewModel.delGroup(item) } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { | ||||
|             val srcItem = getItem(srcPosition) | ||||
|             val targetItem = getItem(targetPosition) | ||||
|             if (srcItem != null && targetItem != null) { | ||||
|                 val order = srcItem.order | ||||
|                 srcItem.order = targetItem.order | ||||
|                 targetItem.order = order | ||||
|                 viewModel.upGroup(srcItem, targetItem) | ||||
|             } | ||||
|             return true | ||||
|         } | ||||
| 
 | ||||
|         override fun onSwiped(adapterPosition: Int) { | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     interface CallBack { | ||||
|         fun upGroup() | ||||
|     } | ||||
| } | ||||
| @ -1,3 +1,70 @@ | ||||
| package io.legado.app.ui.rss.article | ||||
| 
 | ||||
| class RssArticlesActivity | ||||
| import android.os.Bundle | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.lifecycle.LiveData | ||||
| import androidx.lifecycle.Observer | ||||
| import androidx.recyclerview.widget.DividerItemDecoration | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import io.legado.app.App | ||||
| import io.legado.app.R | ||||
| import io.legado.app.base.VMBaseActivity | ||||
| import io.legado.app.data.entities.RssArticle | ||||
| import io.legado.app.lib.theme.ATH | ||||
| import io.legado.app.ui.rss.read.ReadRssActivity | ||||
| import io.legado.app.utils.getViewModel | ||||
| import kotlinx.android.synthetic.main.activity_rss_artivles.* | ||||
| import org.jetbrains.anko.startActivity | ||||
| 
 | ||||
| class RssArticlesActivity : VMBaseActivity<RssArticlesViewModel>(R.layout.activity_rss_artivles), | ||||
|     RssArticlesAdapter.CallBack { | ||||
| 
 | ||||
|     override val viewModel: RssArticlesViewModel | ||||
|         get() = getViewModel(RssArticlesViewModel::class.java) | ||||
| 
 | ||||
|     private var adapter: RssArticlesAdapter? = null | ||||
|     private var rssArticlesData: LiveData<List<RssArticle>>? = null | ||||
| 
 | ||||
|     override fun onActivityCreated(savedInstanceState: Bundle?) { | ||||
|         initView() | ||||
|         viewModel.titleLiveData.observe(this, Observer { | ||||
|             title_bar.title = it | ||||
|         }) | ||||
|         intent.getStringExtra("url")?.let { | ||||
|             initData(it) | ||||
|             viewModel.loadContent(it) { | ||||
|                 refresh_progress_bar.isAutoLoading = false | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun initView() { | ||||
|         ATH.applyEdgeEffectColor(recycler_view) | ||||
|         recycler_view.layoutManager = LinearLayoutManager(this) | ||||
|         recycler_view.addItemDecoration( | ||||
|             DividerItemDecoration(this, DividerItemDecoration.VERTICAL).apply { | ||||
|                 ContextCompat.getDrawable(baseContext, R.drawable.ic_divider)?.let { | ||||
|                     this.setDrawable(it) | ||||
|                 } | ||||
|             }) | ||||
|         adapter = RssArticlesAdapter(this, this) | ||||
|         recycler_view.adapter = adapter | ||||
|         refresh_progress_bar.isAutoLoading = true | ||||
|     } | ||||
| 
 | ||||
|     private fun initData(origin: String) { | ||||
|         rssArticlesData?.removeObservers(this) | ||||
|         rssArticlesData = App.db.rssArtivleDao().liveByOrigin(origin) | ||||
|         rssArticlesData?.observe(this, Observer { | ||||
|             adapter?.setItems(it) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     override fun readRss(rssArticle: RssArticle) { | ||||
|         viewModel.read(rssArticle) | ||||
|         startActivity<ReadRssActivity>( | ||||
|             Pair("description", rssArticle.description), | ||||
|             Pair("url", rssArticle.link) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,44 @@ | ||||
| package io.legado.app.ui.rss.article | ||||
| 
 | ||||
| import android.content.Context | ||||
| import io.legado.app.R | ||||
| import io.legado.app.base.adapter.ItemViewHolder | ||||
| import io.legado.app.base.adapter.SimpleRecyclerAdapter | ||||
| import io.legado.app.data.entities.RssArticle | ||||
| import io.legado.app.help.ImageLoader | ||||
| import io.legado.app.utils.gone | ||||
| import io.legado.app.utils.visible | ||||
| import kotlinx.android.synthetic.main.item_rss_article.view.* | ||||
| import org.jetbrains.anko.sdk27.listeners.onClick | ||||
| import org.jetbrains.anko.textColorResource | ||||
| 
 | ||||
| 
 | ||||
| class RssArticlesAdapter(context: Context, val callBack: CallBack) : | ||||
|     SimpleRecyclerAdapter<RssArticle>(context, R.layout.item_rss_article) { | ||||
| 
 | ||||
|     override fun convert(holder: ItemViewHolder, item: RssArticle, payloads: MutableList<Any>) { | ||||
|         with(holder.itemView) { | ||||
|             tv_title.text = item.title | ||||
|             tv_pub_date.text = item.pubDate | ||||
|             onClick { | ||||
|                 callBack.readRss(item) | ||||
|             } | ||||
|             if (item.image.isNullOrBlank()) { | ||||
|                 image_view.gone() | ||||
|             } else { | ||||
|                 image_view.visible() | ||||
|                 ImageLoader.load(context, item.image) | ||||
|                     .setAsBitmap(image_view) | ||||
|             } | ||||
|             if (item.read) { | ||||
|                 tv_title.textColorResource = R.color.tv_text_summary | ||||
|             } else { | ||||
|                 tv_title.textColorResource = R.color.tv_text_default | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     interface CallBack { | ||||
|         fun readRss(rssArticle: RssArticle) | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,45 @@ | ||||
| package io.legado.app.ui.rss.article | ||||
| 
 | ||||
| import android.app.Application | ||||
| import androidx.lifecycle.MutableLiveData | ||||
| import io.legado.app.App | ||||
| import io.legado.app.base.BaseViewModel | ||||
| import io.legado.app.data.entities.RssArticle | ||||
| import io.legado.app.data.entities.RssSource | ||||
| import io.legado.app.model.Rss | ||||
| import kotlinx.coroutines.Dispatchers.IO | ||||
| 
 | ||||
| 
 | ||||
| class RssArticlesViewModel(application: Application) : BaseViewModel(application) { | ||||
| 
 | ||||
|     val titleLiveData = MutableLiveData<String>() | ||||
| 
 | ||||
|     fun loadContent(url: String, onFinally: () -> Unit) { | ||||
|         execute { | ||||
|             var rssSource = App.db.rssSourceDao().getByKey(url) | ||||
|             if (rssSource == null) { | ||||
|                 rssSource = RssSource(sourceUrl = url) | ||||
|             } else { | ||||
|                 titleLiveData.postValue(rssSource.sourceName) | ||||
|             } | ||||
|             Rss.getArticles(rssSource, this) | ||||
|                 .onSuccess(IO) { | ||||
|                     it?.let { | ||||
|                         App.db.rssArtivleDao().insert(*it.toTypedArray()) | ||||
|                     } | ||||
|                 }.onError { | ||||
|                     toast(it.localizedMessage) | ||||
|                 }.onFinally { | ||||
|                     onFinally() | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun read(rssArticle: RssArticle) { | ||||
|         execute { | ||||
|             rssArticle.read = true | ||||
|             App.db.rssArtivleDao().update(rssArticle) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,2 @@ | ||||
| package io.legado.app.ui.rss.read | ||||
| 
 | ||||
| @ -1,2 +1,16 @@ | ||||
| package io.legado.app.ui.rss.source.debug | ||||
| 
 | ||||
| import android.os.Bundle | ||||
| import io.legado.app.R | ||||
| import io.legado.app.base.BaseActivity | ||||
| 
 | ||||
| 
 | ||||
| class RssSourceDebugActivity : BaseActivity(R.layout.activity_source_debug) { | ||||
| 
 | ||||
| 
 | ||||
|     override fun onActivityCreated(savedInstanceState: Bundle?) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| @ -1,19 +1,212 @@ | ||||
| package io.legado.app.ui.rss.source.edit | ||||
| 
 | ||||
| import android.app.Activity | ||||
| import android.content.ClipData | ||||
| import android.content.ClipboardManager | ||||
| import android.content.Context | ||||
| import android.graphics.Rect | ||||
| import android.os.Bundle | ||||
| import android.view.Gravity | ||||
| import android.view.Menu | ||||
| import android.view.MenuItem | ||||
| import android.view.ViewTreeObserver | ||||
| import android.widget.EditText | ||||
| import android.widget.PopupWindow | ||||
| import androidx.lifecycle.Observer | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import io.legado.app.R | ||||
| import io.legado.app.base.VMBaseActivity | ||||
| import io.legado.app.constant.AppConst | ||||
| import io.legado.app.data.entities.EditEntity | ||||
| import io.legado.app.data.entities.RssSource | ||||
| import io.legado.app.lib.theme.ATH | ||||
| import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity | ||||
| import io.legado.app.ui.widget.KeyboardToolPop | ||||
| import io.legado.app.utils.GSON | ||||
| import io.legado.app.utils.getViewModel | ||||
| import kotlinx.android.synthetic.main.activity_book_source_edit.* | ||||
| import org.jetbrains.anko.displayMetrics | ||||
| import org.jetbrains.anko.startActivity | ||||
| import org.jetbrains.anko.toast | ||||
| import kotlin.math.abs | ||||
| 
 | ||||
| class RssSourceEditActivity : | ||||
|     VMBaseActivity<RssSourceEditViewModel>(R.layout.activity_rss_source_edit, false) { | ||||
|     VMBaseActivity<RssSourceEditViewModel>(R.layout.activity_rss_source_edit, false), | ||||
|     KeyboardToolPop.CallBack { | ||||
| 
 | ||||
|     private var mSoftKeyboardTool: PopupWindow? = null | ||||
|     private var mIsSoftKeyBoardShowing = false | ||||
| 
 | ||||
|     private val adapter = RssSourceEditAdapter() | ||||
|     private val sourceEntities: ArrayList<EditEntity> = ArrayList() | ||||
| 
 | ||||
|     override val viewModel: RssSourceEditViewModel | ||||
|         get() = getViewModel(RssSourceEditViewModel::class.java) | ||||
| 
 | ||||
| 
 | ||||
|     override fun onActivityCreated(savedInstanceState: Bundle?) { | ||||
|         initView() | ||||
|         viewModel.sourceLiveData.observe(this, Observer { | ||||
|             upRecyclerView(it) | ||||
|         }) | ||||
|         viewModel.initData(intent) | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         super.onDestroy() | ||||
|         mSoftKeyboardTool?.dismiss() | ||||
|     } | ||||
| 
 | ||||
|     override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { | ||||
|         menuInflater.inflate(R.menu.source_edit, menu) | ||||
|         return super.onCompatCreateOptionsMenu(menu) | ||||
|     } | ||||
| 
 | ||||
|     override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             R.id.menu_save -> { | ||||
|                 getRssSource()?.let { | ||||
|                     viewModel.save(it) { | ||||
|                         setResult(Activity.RESULT_OK) | ||||
|                         finish() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             R.id.menu_debug_source -> { | ||||
|                 getRssSource()?.let { | ||||
|                     viewModel.save(it) { | ||||
|                         startActivity<RssSourceDebugActivity>(Pair("key", it.sourceUrl)) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             R.id.menu_copy_source -> { | ||||
|                 GSON.toJson(getRssSource())?.let { sourceStr -> | ||||
|                     val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? | ||||
|                     clipboard?.primaryClip = ClipData.newPlainText(null, sourceStr) | ||||
|                 } | ||||
|             } | ||||
|             R.id.menu_paste_source -> viewModel.pasteSource() | ||||
|         } | ||||
|         return super.onCompatOptionsItemSelected(item) | ||||
|     } | ||||
| 
 | ||||
|     private fun initView() { | ||||
|         ATH.applyEdgeEffectColor(recycler_view) | ||||
|         mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this) | ||||
|         window.decorView.viewTreeObserver.addOnGlobalLayoutListener(KeyboardOnGlobalChangeListener()) | ||||
|         recycler_view.layoutManager = LinearLayoutManager(this) | ||||
|         recycler_view.adapter = adapter | ||||
|     } | ||||
| 
 | ||||
|     private fun upRecyclerView(rssSource: RssSource?) { | ||||
|         sourceEntities.clear() | ||||
|         sourceEntities.apply { | ||||
|             add(EditEntity("sourceName", rssSource?.sourceName, R.string.rss_source_name)) | ||||
|             add(EditEntity("sourceUrl", rssSource?.sourceUrl, R.string.rss_source_url)) | ||||
|             add(EditEntity("sourceIcon", rssSource?.sourceIcon, R.string.rss_source_icon)) | ||||
|             add(EditEntity("sourceGroup", rssSource?.sourceGroup, R.string.rss_source_group)) | ||||
|             add(EditEntity("ruleArticles", rssSource?.ruleArticles, R.string.rss_rule_articles)) | ||||
|             add(EditEntity("ruleTitle", rssSource?.ruleTitle, R.string.rss_rule_title)) | ||||
|             add(EditEntity("ruleAuthor", rssSource?.ruleAuthor, R.string.rss_rule_author)) | ||||
|             add(EditEntity("ruleGuid", rssSource?.ruleGuid, R.string.rss_rule_guid)) | ||||
|             add(EditEntity("rulePubDate", rssSource?.rulePubDate, R.string.rss_rule_date)) | ||||
|             add( | ||||
|                 EditEntity( | ||||
|                     "ruleCategories", | ||||
|                     rssSource?.ruleCategories, | ||||
|                     R.string.rss_rule_categories | ||||
|                 ) | ||||
|             ) | ||||
|             add( | ||||
|                 EditEntity( | ||||
|                     "ruleDescription", | ||||
|                     rssSource?.ruleDescription, | ||||
|                     R.string.rss_rule_description | ||||
|                 ) | ||||
|             ) | ||||
|             add(EditEntity("ruleImage", rssSource?.ruleImage, R.string.rss_rule_image)) | ||||
|             add(EditEntity("ruleLink", rssSource?.ruleLink, R.string.rss_rule_link)) | ||||
|             add(EditEntity("ruleContent", rssSource?.ruleContent, R.string.rss_rule_content)) | ||||
|         } | ||||
|         adapter.editEntities = sourceEntities | ||||
|     } | ||||
| 
 | ||||
|     private fun getRssSource(): RssSource? { | ||||
|         val source = viewModel.sourceLiveData.value ?: RssSource() | ||||
|         sourceEntities.forEach { | ||||
|             when (it.key) { | ||||
|                 "sourceName" -> source.sourceName = it.value ?: "" | ||||
|                 "sourceUrl" -> source.sourceUrl = it.value ?: "" | ||||
|                 "sourceIcon" -> source.sourceIcon = it.value ?: "" | ||||
|                 "sourceGroup" -> source.sourceGroup = it.value | ||||
|                 "ruleArticles" -> source.ruleArticles = it.value | ||||
|                 "ruleTitle" -> source.ruleTitle = it.value | ||||
|                 "ruleAuthor" -> source.ruleAuthor = it.value | ||||
|                 "ruleGuid" -> source.ruleGuid = it.value | ||||
|                 "rulePubDate" -> source.rulePubDate = it.value | ||||
|                 "ruleCategories" -> source.ruleCategories = it.value | ||||
|                 "ruleDescription" -> source.ruleDescription = it.value | ||||
|                 "ruleImage" -> source.ruleImage = it.value | ||||
|                 "ruleLink" -> source.ruleLink = it.value | ||||
|                 "ruleContent" -> source.ruleContent = it.value | ||||
|             } | ||||
|         } | ||||
|         if (source.sourceName.isBlank() || source.sourceName.isBlank()) { | ||||
|             toast("名称或url不能为空") | ||||
|             return null | ||||
|         } | ||||
|         return source | ||||
|     } | ||||
| 
 | ||||
|     override fun sendText(text: String) { | ||||
|         if (text.isBlank()) return | ||||
|         val view = window.decorView.findFocus() | ||||
|         if (view is EditText) { | ||||
|             val start = view.selectionStart | ||||
|             val end = view.selectionEnd | ||||
|             val edit = view.editableText//获取EditText的文字 | ||||
|             if (start < 0 || start >= edit.length) { | ||||
|                 edit.append(text) | ||||
|             } else { | ||||
|                 edit.replace(start, end, text)//光标所在位置插入文字 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun showKeyboardTopPopupWindow() { | ||||
|         mSoftKeyboardTool?.isShowing?.let { if (it) return } | ||||
|         if (!isFinishing) { | ||||
|             mSoftKeyboardTool?.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun closePopupWindow() { | ||||
|         mSoftKeyboardTool?.let { | ||||
|             if (it.isShowing) { | ||||
|                 it.dismiss() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener { | ||||
|         override fun onGlobalLayout() { | ||||
|             val rect = Rect() | ||||
|             // 获取当前页面窗口的显示范围 | ||||
|             window.decorView.getWindowVisibleDisplayFrame(rect) | ||||
|             val screenHeight = this@RssSourceEditActivity.displayMetrics.heightPixels | ||||
|             val keyboardHeight = screenHeight - rect.bottom // 输入法的高度 | ||||
|             val preShowing = mIsSoftKeyBoardShowing | ||||
|             if (abs(keyboardHeight) > screenHeight / 5) { | ||||
|                 mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法 | ||||
|                 recycler_view.setPadding(0, 0, 0, 100) | ||||
|                 showKeyboardTopPopupWindow() | ||||
|             } else { | ||||
|                 mIsSoftKeyBoardShowing = false | ||||
|                 recycler_view.setPadding(0, 0, 0, 0) | ||||
|                 if (preShowing) { | ||||
|                     closePopupWindow() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,85 @@ | ||||
| package io.legado.app.ui.rss.source.edit | ||||
| 
 | ||||
| import android.text.Editable | ||||
| import android.text.TextWatcher | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import io.legado.app.R | ||||
| import io.legado.app.data.entities.EditEntity | ||||
| import kotlinx.android.synthetic.main.item_source_edit.view.* | ||||
| 
 | ||||
| class RssSourceEditAdapter : RecyclerView.Adapter<RssSourceEditAdapter.MyViewHolder>() { | ||||
| 
 | ||||
|     var editEntities: ArrayList<EditEntity> = ArrayList() | ||||
|         set(value) { | ||||
|             field = value | ||||
|             notifyDataSetChanged() | ||||
|         } | ||||
| 
 | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { | ||||
|         return MyViewHolder( | ||||
|             LayoutInflater.from( | ||||
|                 parent.context | ||||
|             ).inflate(R.layout.item_source_edit, parent, false) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override fun onBindViewHolder(holder: MyViewHolder, position: Int) { | ||||
|         holder.bind(editEntities[position]) | ||||
|     } | ||||
| 
 | ||||
|     override fun getItemCount(): Int { | ||||
|         return editEntities.size | ||||
|     } | ||||
| 
 | ||||
|     class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) { | ||||
|         fun bind(editEntity: EditEntity) = with(itemView) { | ||||
|             if (editText.getTag(R.id.tag1) == null) { | ||||
|                 val listener = object : View.OnAttachStateChangeListener { | ||||
|                     override fun onViewAttachedToWindow(v: View) { | ||||
|                         editText.isCursorVisible = false | ||||
|                         editText.isCursorVisible = true | ||||
|                         editText.isFocusable = true | ||||
|                         editText.isFocusableInTouchMode = true | ||||
|                     } | ||||
| 
 | ||||
|                     override fun onViewDetachedFromWindow(v: View) { | ||||
| 
 | ||||
|                     } | ||||
|                 } | ||||
|                 editText.addOnAttachStateChangeListener(listener) | ||||
|                 editText.setTag(R.id.tag1, listener) | ||||
|             } | ||||
|             editText.getTag(R.id.tag2)?.let { | ||||
|                 if (it is TextWatcher) { | ||||
|                     editText.removeTextChangedListener(it) | ||||
|                 } | ||||
|             } | ||||
|             editText.setText(editEntity.value) | ||||
|             textInputLayout.hint = context.getString(editEntity.hint) | ||||
|             val textWatcher = object : TextWatcher { | ||||
|                 override fun beforeTextChanged( | ||||
|                     s: CharSequence, | ||||
|                     start: Int, | ||||
|                     count: Int, | ||||
|                     after: Int | ||||
|                 ) { | ||||
| 
 | ||||
|                 } | ||||
| 
 | ||||
|                 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { | ||||
| 
 | ||||
|                 } | ||||
| 
 | ||||
|                 override fun afterTextChanged(s: Editable?) { | ||||
|                     editEntity.value = (s?.toString()) | ||||
|                 } | ||||
|             } | ||||
|             editText.addTextChangedListener(textWatcher) | ||||
|             editText.setTag(R.id.tag2, textWatcher) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -1,9 +1,66 @@ | ||||
| package io.legado.app.ui.rss.source.edit | ||||
| 
 | ||||
| import android.app.Application | ||||
| import android.content.ClipboardManager | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import androidx.lifecycle.MutableLiveData | ||||
| import io.legado.app.App | ||||
| import io.legado.app.base.BaseViewModel | ||||
| import io.legado.app.data.entities.RssSource | ||||
| import io.legado.app.utils.GSON | ||||
| import io.legado.app.utils.fromJsonObject | ||||
| 
 | ||||
| class RssSourceEditViewModel(application: Application) : BaseViewModel(application) { | ||||
| 
 | ||||
|     val sourceLiveData: MutableLiveData<RssSource> = MutableLiveData() | ||||
|     var oldSourceUrl: String? = null | ||||
| 
 | ||||
|     fun initData(intent: Intent) { | ||||
|         execute { | ||||
|             val key = intent.getStringExtra("data") | ||||
|             var source: RssSource? = null | ||||
|             if (key != null) { | ||||
|                 source = App.db.rssSourceDao().getByKey(key) | ||||
|             } | ||||
|             source?.let { | ||||
|                 oldSourceUrl = it.sourceUrl | ||||
|                 sourceLiveData.postValue(it) | ||||
|             } ?: let { | ||||
|                 sourceLiveData.postValue(RssSource().apply { | ||||
|                     customOrder = App.db.rssSourceDao().maxOrder + 1 | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun save(rssSource: RssSource, success: (() -> Unit)) { | ||||
|         execute { | ||||
|             oldSourceUrl?.let { | ||||
|                 if (oldSourceUrl != rssSource.sourceUrl) { | ||||
|                     App.db.rssSourceDao().delete(it) | ||||
|                 } | ||||
|             } | ||||
|             oldSourceUrl = rssSource.sourceUrl | ||||
|             App.db.rssSourceDao().insert(rssSource) | ||||
|         }.onSuccess { | ||||
|             success() | ||||
|         }.onError { | ||||
|             toast(it.localizedMessage) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun pasteSource() { | ||||
|         execute { | ||||
|             val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? | ||||
|             clipboard?.primaryClip?.let { | ||||
|                 if (it.itemCount > 0) { | ||||
|                     val json = it.getItemAt(0).text.toString() | ||||
|                     GSON.fromJsonObject<RssSource>(json)?.let { source -> | ||||
|                         sourceLiveData.postValue(source) | ||||
|                     } ?: toast("格式不对") | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,41 @@ | ||||
| package io.legado.app.ui.rss.source.manage | ||||
| 
 | ||||
| import androidx.recyclerview.widget.DiffUtil | ||||
| import io.legado.app.data.entities.RssSource | ||||
| 
 | ||||
| class DiffCallBack( | ||||
|     private val oldItems: List<RssSource>, | ||||
|     private val newItems: List<RssSource> | ||||
| ) : DiffUtil.Callback() { | ||||
| 
 | ||||
|     override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { | ||||
|         val oldItem = oldItems[oldItemPosition] | ||||
|         val newItem = newItems[newItemPosition] | ||||
|         return oldItem.sourceUrl == newItem.sourceUrl | ||||
|     } | ||||
| 
 | ||||
|     override fun getOldListSize(): Int { | ||||
|         return oldItems.size | ||||
|     } | ||||
| 
 | ||||
|     override fun getNewListSize(): Int { | ||||
|         return newItems.size | ||||
|     } | ||||
| 
 | ||||
|     override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { | ||||
|         val oldItem = oldItems[oldItemPosition] | ||||
|         val newItem = newItems[newItemPosition] | ||||
|         return oldItem.sourceName == newItem.sourceName | ||||
|                 && oldItem.enabled == newItem.enabled | ||||
|     } | ||||
| 
 | ||||
|     override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { | ||||
|         val oldItem = oldItems[oldItemPosition] | ||||
|         val newItem = newItems[newItemPosition] | ||||
|         return when { | ||||
|             oldItem.sourceName == newItem.sourceName | ||||
|                     && oldItem.enabled != newItem.enabled -> 2 | ||||
|             else -> null | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,8 +1,52 @@ | ||||
| package io.legado.app.ui.rss.source.manage | ||||
| 
 | ||||
| import android.app.Application | ||||
| import io.legado.app.App | ||||
| import io.legado.app.base.BaseViewModel | ||||
| import io.legado.app.data.entities.RssSource | ||||
| 
 | ||||
| class RssSourceViewModel(application: Application) : BaseViewModel(application) { | ||||
| 
 | ||||
|     fun topSource(rssSource: RssSource) { | ||||
|         execute { | ||||
|             rssSource.customOrder = App.db.rssSourceDao().minOrder - 1 | ||||
|             App.db.rssSourceDao().insert(rssSource) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun del(rssSource: RssSource) { | ||||
|         execute { App.db.rssSourceDao().delete(rssSource) } | ||||
|     } | ||||
| 
 | ||||
|     fun update(vararg rssSource: RssSource) { | ||||
|         execute { App.db.rssSourceDao().update(*rssSource) } | ||||
|     } | ||||
| 
 | ||||
|     fun upOrder() { | ||||
|         execute { | ||||
|             val sources = App.db.rssSourceDao().all | ||||
|             for ((index: Int, source: RssSource) in sources.withIndex()) { | ||||
|                 source.customOrder = index + 1 | ||||
|             } | ||||
|             App.db.rssSourceDao().update(*sources.toTypedArray()) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun enableSelection(ids: LinkedHashSet<String>) { | ||||
|         execute { | ||||
|             App.db.rssSourceDao().enableSection(*ids.toTypedArray()) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun disableSelection(ids: LinkedHashSet<String>) { | ||||
|         execute { | ||||
|             App.db.rssSourceDao().disableSection(*ids.toTypedArray()) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun delSelection(ids: LinkedHashSet<String>) { | ||||
|         execute { | ||||
|             App.db.rssSourceDao().delSection(*ids.toTypedArray()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB | 
| Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB | 
| After Width: | Height: | Size: 8.9 KiB | 
| @ -1,7 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:orientation="vertical" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
| 
 | ||||
| </LinearLayout> | ||||
| @ -0,0 +1,34 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:orientation="vertical"> | ||||
| 
 | ||||
|     <io.legado.app.ui.widget.TitleBar | ||||
|         android:id="@+id/title_bar" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         app:layout_constraintTop_toTopOf="parent" /> | ||||
| 
 | ||||
|     <io.legado.app.ui.widget.anima.RefreshProgressBar | ||||
|         android:id="@+id/refresh_progress_bar" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="2dp" | ||||
|         app:layout_constraintTop_toBottomOf="@id/title_bar" /> | ||||
| 
 | ||||
|     <io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout | ||||
|         android:id="@+id/content_view" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="0dp" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintTop_toBottomOf="@id/refresh_progress_bar"> | ||||
| 
 | ||||
|         <androidx.recyclerview.widget.RecyclerView | ||||
|             android:id="@+id/recycler_view" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="match_parent" /> | ||||
| 
 | ||||
|     </io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout> | ||||
| 
 | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| @ -0,0 +1,16 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:orientation="vertical"> | ||||
| 
 | ||||
|     <io.legado.app.ui.widget.TitleBar | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" /> | ||||
| 
 | ||||
|     <WebView | ||||
|         android:id="@+id/webView" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" /> | ||||
| 
 | ||||
| </LinearLayout> | ||||
Some files were not shown because too many files have changed in this diff Show More
					Loading…
					
					
				
		Reference in new issue