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