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 |
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 |
package io.legado.app.ui.rss.source.debug |
||||||
|
|
||||||
|
import android.os.Bundle |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.base.BaseActivity |
||||||
|
|
||||||
|
|
||||||
|
class RssSourceDebugActivity : BaseActivity(R.layout.activity_source_debug) { |
||||||
|
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
@ -1,19 +1,212 @@ |
|||||||
package io.legado.app.ui.rss.source.edit |
package io.legado.app.ui.rss.source.edit |
||||||
|
|
||||||
|
import android.app.Activity |
||||||
|
import android.content.ClipData |
||||||
|
import android.content.ClipboardManager |
||||||
|
import android.content.Context |
||||||
|
import android.graphics.Rect |
||||||
import android.os.Bundle |
import android.os.Bundle |
||||||
|
import android.view.Gravity |
||||||
|
import android.view.Menu |
||||||
|
import android.view.MenuItem |
||||||
|
import android.view.ViewTreeObserver |
||||||
|
import android.widget.EditText |
||||||
|
import android.widget.PopupWindow |
||||||
|
import androidx.lifecycle.Observer |
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager |
||||||
import io.legado.app.R |
import io.legado.app.R |
||||||
import io.legado.app.base.VMBaseActivity |
import io.legado.app.base.VMBaseActivity |
||||||
|
import io.legado.app.constant.AppConst |
||||||
|
import io.legado.app.data.entities.EditEntity |
||||||
|
import io.legado.app.data.entities.RssSource |
||||||
|
import io.legado.app.lib.theme.ATH |
||||||
|
import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity |
||||||
|
import io.legado.app.ui.widget.KeyboardToolPop |
||||||
|
import io.legado.app.utils.GSON |
||||||
import io.legado.app.utils.getViewModel |
import io.legado.app.utils.getViewModel |
||||||
|
import kotlinx.android.synthetic.main.activity_book_source_edit.* |
||||||
|
import org.jetbrains.anko.displayMetrics |
||||||
|
import org.jetbrains.anko.startActivity |
||||||
|
import org.jetbrains.anko.toast |
||||||
|
import kotlin.math.abs |
||||||
|
|
||||||
class RssSourceEditActivity : |
class RssSourceEditActivity : |
||||||
VMBaseActivity<RssSourceEditViewModel>(R.layout.activity_rss_source_edit, false) { |
VMBaseActivity<RssSourceEditViewModel>(R.layout.activity_rss_source_edit, false), |
||||||
|
KeyboardToolPop.CallBack { |
||||||
|
|
||||||
|
private var mSoftKeyboardTool: PopupWindow? = null |
||||||
|
private var mIsSoftKeyBoardShowing = false |
||||||
|
|
||||||
|
private val adapter = RssSourceEditAdapter() |
||||||
|
private val sourceEntities: ArrayList<EditEntity> = ArrayList() |
||||||
|
|
||||||
override val viewModel: RssSourceEditViewModel |
override val viewModel: RssSourceEditViewModel |
||||||
get() = getViewModel(RssSourceEditViewModel::class.java) |
get() = getViewModel(RssSourceEditViewModel::class.java) |
||||||
|
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) { |
override fun onActivityCreated(savedInstanceState: Bundle?) { |
||||||
|
initView() |
||||||
|
viewModel.sourceLiveData.observe(this, Observer { |
||||||
|
upRecyclerView(it) |
||||||
|
}) |
||||||
|
viewModel.initData(intent) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onDestroy() { |
||||||
|
super.onDestroy() |
||||||
|
mSoftKeyboardTool?.dismiss() |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { |
||||||
|
menuInflater.inflate(R.menu.source_edit, menu) |
||||||
|
return super.onCompatCreateOptionsMenu(menu) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { |
||||||
|
when (item.itemId) { |
||||||
|
R.id.menu_save -> { |
||||||
|
getRssSource()?.let { |
||||||
|
viewModel.save(it) { |
||||||
|
setResult(Activity.RESULT_OK) |
||||||
|
finish() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
R.id.menu_debug_source -> { |
||||||
|
getRssSource()?.let { |
||||||
|
viewModel.save(it) { |
||||||
|
startActivity<RssSourceDebugActivity>(Pair("key", it.sourceUrl)) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
R.id.menu_copy_source -> { |
||||||
|
GSON.toJson(getRssSource())?.let { sourceStr -> |
||||||
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? |
||||||
|
clipboard?.primaryClip = ClipData.newPlainText(null, sourceStr) |
||||||
|
} |
||||||
|
} |
||||||
|
R.id.menu_paste_source -> viewModel.pasteSource() |
||||||
|
} |
||||||
|
return super.onCompatOptionsItemSelected(item) |
||||||
|
} |
||||||
|
|
||||||
|
private fun initView() { |
||||||
|
ATH.applyEdgeEffectColor(recycler_view) |
||||||
|
mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this) |
||||||
|
window.decorView.viewTreeObserver.addOnGlobalLayoutListener(KeyboardOnGlobalChangeListener()) |
||||||
|
recycler_view.layoutManager = LinearLayoutManager(this) |
||||||
|
recycler_view.adapter = adapter |
||||||
|
} |
||||||
|
|
||||||
|
private fun upRecyclerView(rssSource: RssSource?) { |
||||||
|
sourceEntities.clear() |
||||||
|
sourceEntities.apply { |
||||||
|
add(EditEntity("sourceName", rssSource?.sourceName, R.string.rss_source_name)) |
||||||
|
add(EditEntity("sourceUrl", rssSource?.sourceUrl, R.string.rss_source_url)) |
||||||
|
add(EditEntity("sourceIcon", rssSource?.sourceIcon, R.string.rss_source_icon)) |
||||||
|
add(EditEntity("sourceGroup", rssSource?.sourceGroup, R.string.rss_source_group)) |
||||||
|
add(EditEntity("ruleArticles", rssSource?.ruleArticles, R.string.rss_rule_articles)) |
||||||
|
add(EditEntity("ruleTitle", rssSource?.ruleTitle, R.string.rss_rule_title)) |
||||||
|
add(EditEntity("ruleAuthor", rssSource?.ruleAuthor, R.string.rss_rule_author)) |
||||||
|
add(EditEntity("ruleGuid", rssSource?.ruleGuid, R.string.rss_rule_guid)) |
||||||
|
add(EditEntity("rulePubDate", rssSource?.rulePubDate, R.string.rss_rule_date)) |
||||||
|
add( |
||||||
|
EditEntity( |
||||||
|
"ruleCategories", |
||||||
|
rssSource?.ruleCategories, |
||||||
|
R.string.rss_rule_categories |
||||||
|
) |
||||||
|
) |
||||||
|
add( |
||||||
|
EditEntity( |
||||||
|
"ruleDescription", |
||||||
|
rssSource?.ruleDescription, |
||||||
|
R.string.rss_rule_description |
||||||
|
) |
||||||
|
) |
||||||
|
add(EditEntity("ruleImage", rssSource?.ruleImage, R.string.rss_rule_image)) |
||||||
|
add(EditEntity("ruleLink", rssSource?.ruleLink, R.string.rss_rule_link)) |
||||||
|
add(EditEntity("ruleContent", rssSource?.ruleContent, R.string.rss_rule_content)) |
||||||
|
} |
||||||
|
adapter.editEntities = sourceEntities |
||||||
} |
} |
||||||
|
|
||||||
|
private fun getRssSource(): RssSource? { |
||||||
|
val source = viewModel.sourceLiveData.value ?: RssSource() |
||||||
|
sourceEntities.forEach { |
||||||
|
when (it.key) { |
||||||
|
"sourceName" -> source.sourceName = it.value ?: "" |
||||||
|
"sourceUrl" -> source.sourceUrl = it.value ?: "" |
||||||
|
"sourceIcon" -> source.sourceIcon = it.value ?: "" |
||||||
|
"sourceGroup" -> source.sourceGroup = it.value |
||||||
|
"ruleArticles" -> source.ruleArticles = it.value |
||||||
|
"ruleTitle" -> source.ruleTitle = it.value |
||||||
|
"ruleAuthor" -> source.ruleAuthor = it.value |
||||||
|
"ruleGuid" -> source.ruleGuid = it.value |
||||||
|
"rulePubDate" -> source.rulePubDate = it.value |
||||||
|
"ruleCategories" -> source.ruleCategories = it.value |
||||||
|
"ruleDescription" -> source.ruleDescription = it.value |
||||||
|
"ruleImage" -> source.ruleImage = it.value |
||||||
|
"ruleLink" -> source.ruleLink = it.value |
||||||
|
"ruleContent" -> source.ruleContent = it.value |
||||||
|
} |
||||||
|
} |
||||||
|
if (source.sourceName.isBlank() || source.sourceName.isBlank()) { |
||||||
|
toast("名称或url不能为空") |
||||||
|
return null |
||||||
|
} |
||||||
|
return source |
||||||
|
} |
||||||
|
|
||||||
|
override fun sendText(text: String) { |
||||||
|
if (text.isBlank()) return |
||||||
|
val view = window.decorView.findFocus() |
||||||
|
if (view is EditText) { |
||||||
|
val start = view.selectionStart |
||||||
|
val end = view.selectionEnd |
||||||
|
val edit = view.editableText//获取EditText的文字 |
||||||
|
if (start < 0 || start >= edit.length) { |
||||||
|
edit.append(text) |
||||||
|
} else { |
||||||
|
edit.replace(start, end, text)//光标所在位置插入文字 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun showKeyboardTopPopupWindow() { |
||||||
|
mSoftKeyboardTool?.isShowing?.let { if (it) return } |
||||||
|
if (!isFinishing) { |
||||||
|
mSoftKeyboardTool?.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun closePopupWindow() { |
||||||
|
mSoftKeyboardTool?.let { |
||||||
|
if (it.isShowing) { |
||||||
|
it.dismiss() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener { |
||||||
|
override fun onGlobalLayout() { |
||||||
|
val rect = Rect() |
||||||
|
// 获取当前页面窗口的显示范围 |
||||||
|
window.decorView.getWindowVisibleDisplayFrame(rect) |
||||||
|
val screenHeight = this@RssSourceEditActivity.displayMetrics.heightPixels |
||||||
|
val keyboardHeight = screenHeight - rect.bottom // 输入法的高度 |
||||||
|
val preShowing = mIsSoftKeyBoardShowing |
||||||
|
if (abs(keyboardHeight) > screenHeight / 5) { |
||||||
|
mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法 |
||||||
|
recycler_view.setPadding(0, 0, 0, 100) |
||||||
|
showKeyboardTopPopupWindow() |
||||||
|
} else { |
||||||
|
mIsSoftKeyBoardShowing = false |
||||||
|
recycler_view.setPadding(0, 0, 0, 0) |
||||||
|
if (preShowing) { |
||||||
|
closePopupWindow() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
@ -0,0 +1,85 @@ |
|||||||
|
package io.legado.app.ui.rss.source.edit |
||||||
|
|
||||||
|
import android.text.Editable |
||||||
|
import android.text.TextWatcher |
||||||
|
import android.view.LayoutInflater |
||||||
|
import android.view.View |
||||||
|
import android.view.ViewGroup |
||||||
|
import androidx.recyclerview.widget.RecyclerView |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.data.entities.EditEntity |
||||||
|
import kotlinx.android.synthetic.main.item_source_edit.view.* |
||||||
|
|
||||||
|
class RssSourceEditAdapter : RecyclerView.Adapter<RssSourceEditAdapter.MyViewHolder>() { |
||||||
|
|
||||||
|
var editEntities: ArrayList<EditEntity> = ArrayList() |
||||||
|
set(value) { |
||||||
|
field = value |
||||||
|
notifyDataSetChanged() |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { |
||||||
|
return MyViewHolder( |
||||||
|
LayoutInflater.from( |
||||||
|
parent.context |
||||||
|
).inflate(R.layout.item_source_edit, parent, false) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MyViewHolder, position: Int) { |
||||||
|
holder.bind(editEntities[position]) |
||||||
|
} |
||||||
|
|
||||||
|
override fun getItemCount(): Int { |
||||||
|
return editEntities.size |
||||||
|
} |
||||||
|
|
||||||
|
class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) { |
||||||
|
fun bind(editEntity: EditEntity) = with(itemView) { |
||||||
|
if (editText.getTag(R.id.tag1) == null) { |
||||||
|
val listener = object : View.OnAttachStateChangeListener { |
||||||
|
override fun onViewAttachedToWindow(v: View) { |
||||||
|
editText.isCursorVisible = false |
||||||
|
editText.isCursorVisible = true |
||||||
|
editText.isFocusable = true |
||||||
|
editText.isFocusableInTouchMode = true |
||||||
|
} |
||||||
|
|
||||||
|
override fun onViewDetachedFromWindow(v: View) { |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
editText.addOnAttachStateChangeListener(listener) |
||||||
|
editText.setTag(R.id.tag1, listener) |
||||||
|
} |
||||||
|
editText.getTag(R.id.tag2)?.let { |
||||||
|
if (it is TextWatcher) { |
||||||
|
editText.removeTextChangedListener(it) |
||||||
|
} |
||||||
|
} |
||||||
|
editText.setText(editEntity.value) |
||||||
|
textInputLayout.hint = context.getString(editEntity.hint) |
||||||
|
val textWatcher = object : TextWatcher { |
||||||
|
override fun beforeTextChanged( |
||||||
|
s: CharSequence, |
||||||
|
start: Int, |
||||||
|
count: Int, |
||||||
|
after: Int |
||||||
|
) { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
override fun afterTextChanged(s: Editable?) { |
||||||
|
editEntity.value = (s?.toString()) |
||||||
|
} |
||||||
|
} |
||||||
|
editText.addTextChangedListener(textWatcher) |
||||||
|
editText.setTag(R.id.tag2, textWatcher) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1,9 +1,66 @@ |
|||||||
package io.legado.app.ui.rss.source.edit |
package io.legado.app.ui.rss.source.edit |
||||||
|
|
||||||
import android.app.Application |
import android.app.Application |
||||||
|
import android.content.ClipboardManager |
||||||
|
import android.content.Context |
||||||
|
import android.content.Intent |
||||||
|
import androidx.lifecycle.MutableLiveData |
||||||
|
import io.legado.app.App |
||||||
import io.legado.app.base.BaseViewModel |
import io.legado.app.base.BaseViewModel |
||||||
|
import io.legado.app.data.entities.RssSource |
||||||
|
import io.legado.app.utils.GSON |
||||||
|
import io.legado.app.utils.fromJsonObject |
||||||
|
|
||||||
class RssSourceEditViewModel(application: Application) : BaseViewModel(application) { |
class RssSourceEditViewModel(application: Application) : BaseViewModel(application) { |
||||||
|
|
||||||
|
val sourceLiveData: MutableLiveData<RssSource> = MutableLiveData() |
||||||
|
var oldSourceUrl: String? = null |
||||||
|
|
||||||
|
fun initData(intent: Intent) { |
||||||
|
execute { |
||||||
|
val key = intent.getStringExtra("data") |
||||||
|
var source: RssSource? = null |
||||||
|
if (key != null) { |
||||||
|
source = App.db.rssSourceDao().getByKey(key) |
||||||
|
} |
||||||
|
source?.let { |
||||||
|
oldSourceUrl = it.sourceUrl |
||||||
|
sourceLiveData.postValue(it) |
||||||
|
} ?: let { |
||||||
|
sourceLiveData.postValue(RssSource().apply { |
||||||
|
customOrder = App.db.rssSourceDao().maxOrder + 1 |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun save(rssSource: RssSource, success: (() -> Unit)) { |
||||||
|
execute { |
||||||
|
oldSourceUrl?.let { |
||||||
|
if (oldSourceUrl != rssSource.sourceUrl) { |
||||||
|
App.db.rssSourceDao().delete(it) |
||||||
|
} |
||||||
|
} |
||||||
|
oldSourceUrl = rssSource.sourceUrl |
||||||
|
App.db.rssSourceDao().insert(rssSource) |
||||||
|
}.onSuccess { |
||||||
|
success() |
||||||
|
}.onError { |
||||||
|
toast(it.localizedMessage) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun pasteSource() { |
||||||
|
execute { |
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? |
||||||
|
clipboard?.primaryClip?.let { |
||||||
|
if (it.itemCount > 0) { |
||||||
|
val json = it.getItemAt(0).text.toString() |
||||||
|
GSON.fromJsonObject<RssSource>(json)?.let { source -> |
||||||
|
sourceLiveData.postValue(source) |
||||||
|
} ?: toast("格式不对") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
@ -0,0 +1,41 @@ |
|||||||
|
package io.legado.app.ui.rss.source.manage |
||||||
|
|
||||||
|
import androidx.recyclerview.widget.DiffUtil |
||||||
|
import io.legado.app.data.entities.RssSource |
||||||
|
|
||||||
|
class DiffCallBack( |
||||||
|
private val oldItems: List<RssSource>, |
||||||
|
private val newItems: List<RssSource> |
||||||
|
) : DiffUtil.Callback() { |
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { |
||||||
|
val oldItem = oldItems[oldItemPosition] |
||||||
|
val newItem = newItems[newItemPosition] |
||||||
|
return oldItem.sourceUrl == newItem.sourceUrl |
||||||
|
} |
||||||
|
|
||||||
|
override fun getOldListSize(): Int { |
||||||
|
return oldItems.size |
||||||
|
} |
||||||
|
|
||||||
|
override fun getNewListSize(): Int { |
||||||
|
return newItems.size |
||||||
|
} |
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { |
||||||
|
val oldItem = oldItems[oldItemPosition] |
||||||
|
val newItem = newItems[newItemPosition] |
||||||
|
return oldItem.sourceName == newItem.sourceName |
||||||
|
&& oldItem.enabled == newItem.enabled |
||||||
|
} |
||||||
|
|
||||||
|
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { |
||||||
|
val oldItem = oldItems[oldItemPosition] |
||||||
|
val newItem = newItems[newItemPosition] |
||||||
|
return when { |
||||||
|
oldItem.sourceName == newItem.sourceName |
||||||
|
&& oldItem.enabled != newItem.enabled -> 2 |
||||||
|
else -> null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,8 +1,52 @@ |
|||||||
package io.legado.app.ui.rss.source.manage |
package io.legado.app.ui.rss.source.manage |
||||||
|
|
||||||
import android.app.Application |
import android.app.Application |
||||||
|
import io.legado.app.App |
||||||
import io.legado.app.base.BaseViewModel |
import io.legado.app.base.BaseViewModel |
||||||
|
import io.legado.app.data.entities.RssSource |
||||||
|
|
||||||
class RssSourceViewModel(application: Application) : BaseViewModel(application) { |
class RssSourceViewModel(application: Application) : BaseViewModel(application) { |
||||||
|
|
||||||
|
fun topSource(rssSource: RssSource) { |
||||||
|
execute { |
||||||
|
rssSource.customOrder = App.db.rssSourceDao().minOrder - 1 |
||||||
|
App.db.rssSourceDao().insert(rssSource) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun del(rssSource: RssSource) { |
||||||
|
execute { App.db.rssSourceDao().delete(rssSource) } |
||||||
|
} |
||||||
|
|
||||||
|
fun update(vararg rssSource: RssSource) { |
||||||
|
execute { App.db.rssSourceDao().update(*rssSource) } |
||||||
|
} |
||||||
|
|
||||||
|
fun upOrder() { |
||||||
|
execute { |
||||||
|
val sources = App.db.rssSourceDao().all |
||||||
|
for ((index: Int, source: RssSource) in sources.withIndex()) { |
||||||
|
source.customOrder = index + 1 |
||||||
|
} |
||||||
|
App.db.rssSourceDao().update(*sources.toTypedArray()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun enableSelection(ids: LinkedHashSet<String>) { |
||||||
|
execute { |
||||||
|
App.db.rssSourceDao().enableSection(*ids.toTypedArray()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun disableSelection(ids: LinkedHashSet<String>) { |
||||||
|
execute { |
||||||
|
App.db.rssSourceDao().disableSection(*ids.toTypedArray()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun delSelection(ids: LinkedHashSet<String>) { |
||||||
|
execute { |
||||||
|
App.db.rssSourceDao().delSection(*ids.toTypedArray()) |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
@ -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