From 7793d547c4e2cd57d0244a260752a3a14866c1a6 Mon Sep 17 00:00:00 2001 From: Michael Lien Date: Sun, 24 Jan 2021 17:50:26 +0800 Subject: [PATCH] Refactor ArticleListFragment.kt and ArticleReadFragment.kt to dependency injection with Koin --- .../java/tw/y_studio/ptt/di/AppModules.kt | 7 + .../main/java/tw/y_studio/ptt/di/Injection.kt | 6 - .../tw/y_studio/ptt/di/ViewModelModules.kt | 4 + .../ui/article/list/ArticleListFragment.kt | 31 +- .../ui/article/list/ArticleListViewModel.kt | 14 +- .../ui/article/read/ArticleReadFragment.kt | 289 ++++-------------- .../ui/article/read/ArticleReadViewModel.kt | 215 +++++++++++++ 7 files changed, 293 insertions(+), 273 deletions(-) create mode 100644 app/src/main/java/tw/y_studio/ptt/ui/article/read/ArticleReadViewModel.kt diff --git a/app/src/main/java/tw/y_studio/ptt/di/AppModules.kt b/app/src/main/java/tw/y_studio/ptt/di/AppModules.kt index 02ab673..d00e3f7 100644 --- a/app/src/main/java/tw/y_studio/ptt/di/AppModules.kt +++ b/app/src/main/java/tw/y_studio/ptt/di/AppModules.kt @@ -1,10 +1,17 @@ package tw.y_studio.ptt.di +import android.content.Context +import android.content.SharedPreferences import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import org.koin.android.ext.koin.androidContext import org.koin.core.qualifier.named import org.koin.dsl.module val appModules = module { single(named("IO")) { Dispatchers.IO } + + single { + androidContext().getSharedPreferences("MainSetting", Context.MODE_PRIVATE) + } } diff --git a/app/src/main/java/tw/y_studio/ptt/di/Injection.kt b/app/src/main/java/tw/y_studio/ptt/di/Injection.kt index 641f194..0124f79 100644 --- a/app/src/main/java/tw/y_studio/ptt/di/Injection.kt +++ b/app/src/main/java/tw/y_studio/ptt/di/Injection.kt @@ -1,23 +1,17 @@ package tw.y_studio.ptt.di -import tw.y_studio.ptt.api.PostAPI import tw.y_studio.ptt.api.SearchBoardAPI -import tw.y_studio.ptt.source.remote.post.PostRemoteDataSourceImpl import tw.y_studio.ptt.source.remote.search.ISearchBoardRemoteDataSource import tw.y_studio.ptt.source.remote.search.SearchBoardRemoteDataSourceImpl object Injection { object API { val searchBoardAPI by lazy { SearchBoardAPI() } - val postAPI by lazy { PostAPI() } } object RemoteDataSource { val searchBoardRemoteDataSource: ISearchBoardRemoteDataSource by lazy { SearchBoardRemoteDataSourceImpl(API.searchBoardAPI) } - val postRemoteDataSourceImpl by lazy { - PostRemoteDataSourceImpl(API.postAPI) - } } } diff --git a/app/src/main/java/tw/y_studio/ptt/di/ViewModelModules.kt b/app/src/main/java/tw/y_studio/ptt/di/ViewModelModules.kt index a05c8a7..f2e7282 100644 --- a/app/src/main/java/tw/y_studio/ptt/di/ViewModelModules.kt +++ b/app/src/main/java/tw/y_studio/ptt/di/ViewModelModules.kt @@ -3,8 +3,12 @@ package tw.y_studio.ptt.di import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module +import tw.y_studio.ptt.ui.article.list.ArticleListViewModel +import tw.y_studio.ptt.ui.article.read.ArticleReadViewModel import tw.y_studio.ptt.ui.hot_board.HotBoardsViewModel val viewModelModules = module { viewModel { HotBoardsViewModel(get(), get(named("IO"))) } + viewModel { ArticleListViewModel(get(), get(named("IO"))) } + viewModel { ArticleReadViewModel(get(), get(), get(named("IO"))) } } diff --git a/app/src/main/java/tw/y_studio/ptt/ui/article/list/ArticleListFragment.kt b/app/src/main/java/tw/y_studio/ptt/ui/article/list/ArticleListFragment.kt index 65516cd..2c5617b 100644 --- a/app/src/main/java/tw/y_studio/ptt/ui/article/list/ArticleListFragment.kt +++ b/app/src/main/java/tw/y_studio/ptt/ui/article/list/ArticleListFragment.kt @@ -5,13 +5,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomnavigation.BottomNavigationView +import org.koin.androidx.viewmodel.ext.android.viewModel import tw.y_studio.ptt.R import tw.y_studio.ptt.databinding.ArticleListFragmentLayoutBinding -import tw.y_studio.ptt.di.Injection import tw.y_studio.ptt.fragment.ArticleListSearchFragment import tw.y_studio.ptt.fragment.PostArticleFragment import tw.y_studio.ptt.model.PartialPost @@ -31,13 +29,14 @@ class ArticleListFragment : BaseFragment() { private var boardSubName = "" private val mClickFix = ClickFix() - private lateinit var articleListViewModel: ArticleListViewModel + private val articleListViewModel: ArticleListViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val bundle = arguments // 取得Bundle boardName = bundle?.getString("title", getString(R.string.board_list_title_empty)) ?: "" - boardSubName = bundle?.getString("subtitle", getString(R.string.board_list_subtitle_empty)) ?: "" + boardSubName = bundle?.getString("subtitle", getString(R.string.board_list_subtitle_empty)) + ?: "" } override fun onCreateView( @@ -51,20 +50,6 @@ class ArticleListFragment : BaseFragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - - articleListViewModel = ViewModelProvider( - this, - object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return ArticleListViewModel( - Injection.RemoteDataSource.postRemoteDataSourceImpl, - boardName - ) as T - } - } - ).get(ArticleListViewModel::class.java) - binding.apply { articleListFragmentTextViewTitle.text = boardName articleListFragmentRecyclerView.apply { @@ -100,7 +85,7 @@ class ArticleListFragment : BaseFragment() { val lastVisibleItem = layoutManager.findLastVisibleItemPosition() val totalItemCount = layoutManager.itemCount if (lastVisibleItem >= totalItemCount - 30) { - articleListViewModel.loadNextData() + articleListViewModel.loadNextData(boardName) } } }) @@ -112,13 +97,13 @@ class ArticleListFragment : BaseFragment() { android.R.color.holo_green_light, android.R.color.holo_orange_light ) - setOnRefreshListener { articleListViewModel.loadData() } + setOnRefreshListener { articleListViewModel.loadData(boardName) } } articleListFragmentBottomNavigation.setOnNavigationItemSelectedListener( BottomNavigationView.OnNavigationItemSelectedListener { item -> when (item.itemId) { R.id.article_list_navigation_item_refresh -> { - articleListViewModel.loadData() + articleListViewModel.loadData(boardName) return@OnNavigationItemSelectedListener false } R.id.article_list_navigation_item_post -> { @@ -158,7 +143,7 @@ class ArticleListFragment : BaseFragment() { } override fun onAnimOver() { - articleListViewModel.loadData() + articleListViewModel.loadData(boardName) binding.articleListFragmentRecyclerView.adapter?.notifyDataSetChanged() } diff --git a/app/src/main/java/tw/y_studio/ptt/ui/article/list/ArticleListViewModel.kt b/app/src/main/java/tw/y_studio/ptt/ui/article/list/ArticleListViewModel.kt index 263a9da..69fc928 100644 --- a/app/src/main/java/tw/y_studio/ptt/ui/article/list/ArticleListViewModel.kt +++ b/app/src/main/java/tw/y_studio/ptt/ui/article/list/ArticleListViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import tw.y_studio.ptt.model.PartialPost @@ -15,7 +15,7 @@ import java.util.concurrent.atomic.AtomicInteger class ArticleListViewModel( private val postRemoteDataSource: IPostRemoteDataSource, - private val boardName: String // TODO: 2020/11/21 need refactor + private val ioDispatcher: CoroutineDispatcher ) : ViewModel() { val data: MutableList = ArrayList() private val page = AtomicInteger(1) @@ -27,7 +27,7 @@ class ArticleListViewModel( fun getErrorLiveData(): LiveData = errorLiveData - fun loadData() { + fun loadData(boardName: String) { viewModelScope.launch { if (loadingState.value == true) { return@launch @@ -35,23 +35,23 @@ class ArticleListViewModel( data.clear() page.set(1) loadingState.value = true - getDataFromApi() + getDataFromApi(boardName) loadingState.value = false } } - fun loadNextData() { + fun loadNextData(boardName: String) { viewModelScope.launch { if (loadingState.value == true) { return@launch } loadingState.value = true - getDataFromApi() + getDataFromApi(boardName) loadingState.value = false } } - private suspend fun getDataFromApi() = withContext(Dispatchers.Default) { + private suspend fun getDataFromApi(boardName: String) = withContext(ioDispatcher) { try { val temp = mutableListOf() for (i in 0..2) { diff --git a/app/src/main/java/tw/y_studio/ptt/ui/article/read/ArticleReadFragment.kt b/app/src/main/java/tw/y_studio/ptt/ui/article/read/ArticleReadFragment.kt index 7e9debe..63d0d6e 100644 --- a/app/src/main/java/tw/y_studio/ptt/ui/article/read/ArticleReadFragment.kt +++ b/app/src/main/java/tw/y_studio/ptt/ui/article/read/ArticleReadFragment.kt @@ -2,37 +2,37 @@ package tw.y_studio.ptt.ui.article.read import android.app.ProgressDialog import android.content.Context +import android.content.SharedPreferences import android.os.Bundle -import android.os.Handler -import android.os.HandlerThread import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager -import android.widget.* +import android.widget.Toast import androidx.annotation.ColorInt import androidx.appcompat.widget.PopupMenu import androidx.recyclerview.widget.RecyclerView +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel import tw.y_studio.ptt.R import tw.y_studio.ptt.api.PostRankMark import tw.y_studio.ptt.databinding.ArticleReadFragmentLayoutBinding -import tw.y_studio.ptt.di.Injection import tw.y_studio.ptt.fragment.LoginPageFragment import tw.y_studio.ptt.ptt.AidConverter -import tw.y_studio.ptt.source.remote.post.IPostRemoteDataSource import tw.y_studio.ptt.ui.BaseFragment import tw.y_studio.ptt.ui.CustomLinearLayoutManager -import tw.y_studio.ptt.utils.* -import java.util.* +import tw.y_studio.ptt.utils.ResourcesUtils +import tw.y_studio.ptt.utils.observeNotNull +import tw.y_studio.ptt.utils.shareTo +import tw.y_studio.ptt.utils.useApi import java.util.regex.Pattern class ArticleReadFragment : BaseFragment() { private var _binding: ArticleReadFragmentLayoutBinding? = null private val binding get() = _binding!! private val urlPattern = Pattern.compile("www.ptt.cc/bbs/([-a-zA-Z0-9_]{2,})/([M|G].[-a-zA-Z0-9._]{1,30}).htm") - private var mAdapter: ArticleReadAdapter? = null - private val data: MutableList = ArrayList() + private var adapter: ArticleReadAdapter? = null private var fileName = "" private var board = "" private var aid = "" @@ -41,13 +41,21 @@ class ArticleReadFragment : BaseFragment() { private var articleAuth = " " private var articleTime = "" private var articleClass = "" - private var originalArticleTitle = "" + private var orgUrl = "" + + private val haveApi = true + + private var progressDialog: ProgressDialog? = null + + private val viewModel: ArticleReadViewModel by viewModel() + + private val preferences: SharedPreferences by inject() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { return ArticleReadFragmentLayoutBinding.inflate(inflater, container, false).apply { _binding = this }.root @@ -78,21 +86,21 @@ class ArticleReadFragment : BaseFragment() { articleReadItemImageButtonShare.setOnClickListener { shareTo( requireContext(), - originalArticleTitle, + viewModel.originalArticleTitle, """ - $originalArticleTitle + ${viewModel.originalArticleTitle} $orgUrl """.trimIndent(), "分享文章" ) } articleReadFragmentRecyclerView.apply { - mAdapter = ArticleReadAdapter(data) + this@ArticleReadFragment.adapter = ArticleReadAdapter(viewModel.data) setHasFixedSize(true) val layoutManager = CustomLinearLayoutManager(context) layoutManager.orientation = RecyclerView.VERTICAL setLayoutManager(layoutManager) - adapter = mAdapter + adapter = this@ArticleReadFragment.adapter } articleReadFragmentRefreshLayout.apply { setColorSchemeResources( @@ -102,10 +110,25 @@ class ArticleReadFragment : BaseFragment() { android.R.color.holo_orange_light ) setOnRefreshListener { - loadData() + viewModel.loadData(board, fileName, aid, articleBoard) } } } + viewModel.apply { + observeNotNull(loadingState) { + binding.articleReadFragmentRefreshLayout.isRefreshing = it + binding.articleReadFragmentRecyclerView.adapter?.notifyDataSetChanged() + } + observeNotNull(errorMessage) { + Toast.makeText(currentActivity, "Error : $it", Toast.LENGTH_SHORT).show() + } + observeNotNull(progressDialogState) { + progressDialog?.dismiss() + } + observeNotNull(likeNumber) { + binding.articleReadItemTextViewLike.text = it + } + } val window = currentActivity.window window.statusBarColor = ResourcesUtils.getColor(requireContext(), R.attr.article_header) @@ -123,139 +146,19 @@ class ArticleReadFragment : BaseFragment() { articleAuth = bundle.getString("auth", "") articleClass = bundle.getString("class", "") articleTime = bundle.getString("date", "") - putDefaultHeader() + viewModel.createDefaultHeader(articleTitle, articleAuth, articleTime, articleClass, articleBoard) + viewModel.putDefaultHeader() } override fun onAnimOver() { - loadData() - } - - private var mThreadHandler: Handler? = null - private var mThread: HandlerThread? = null - private lateinit var r1: Runnable - private var orgUrl = "" - private var floorNum = 0 - private var postRemoteDataSource: IPostRemoteDataSource = Injection.RemoteDataSource.postRemoteDataSourceImpl - - // mAdapter.notifyDataSetChanged(); - private val dataFromApi: Unit - private get() { - r1 = Runnable { - runOnUI { binding.articleReadFragmentRefreshLayout.isRefreshing = true } - GattingData = true - Log("onAR", "get data from web start") - try { - val post = postRemoteDataSource.getPost(board, fileName) - val postRank = postRemoteDataSource.getPostRank(board, aid) - floorNum = post.comments.size - originalArticleTitle = post.title - val dataTemp = mutableListOf() - dataTemp.add( - ArticleReadAdapter.Item.HeaderItem( - post.title, - "${post.auth} (${post.authNickName})", - post.date, - post.classString, - articleBoard - ) - ) - - val contents = post.content.split("\r\n".toRegex()).toTypedArray() - var contentTemp = StringBuilder() - for (i in contents.indices) { - val cmd = contents[i] - val urlM = StringUtils.UrlPattern.matcher(cmd) - if (urlM.find()) { - dataTemp.add(ArticleReadAdapter.Item.ContentLineItem(contentTemp.toString())) - contentTemp = StringBuilder() - dataTemp.add(ArticleReadAdapter.Item.ContentLineItem(cmd)) - val imageUrl = StringUtils.getImgUrl(cmd) - for (urlString in imageUrl) { - dataTemp.add(ArticleReadAdapter.Item.ImageItem(-2, urlString)) - } - } else { - contentTemp.append(cmd) - if (i < contents.size - 1) { - contentTemp.append("\n") - } - } - } - if (contentTemp.toString().isNotEmpty()) { - dataTemp.add(ArticleReadAdapter.Item.ContentLineItem(contentTemp.toString())) - } - dataTemp.add(ArticleReadAdapter.Item.CenterBarItem(postRank.getLike().toString(), floorNum.toString())) - post.comments.forEachIndexed { index, comment -> - dataTemp.add(ArticleReadAdapter.Item.CommentItem(index, comment.content, comment.userid)) - val imageUrl: List = StringUtils.getImgUrl(StringUtils.notNullString(comment.content)) - for (urlString in imageUrl) { - dataTemp.add(ArticleReadAdapter.Item.ImageItem(index, urlString)) - } - dataTemp.add( - ArticleReadAdapter.Item.CommentBarItem( - index, - comment.date, - "${index + 1}F", - "0" - ) - ) - } - - runOnUI { - data.clear() - data.addAll(dataTemp) - mAdapter!!.notifyDataSetChanged() - binding.articleReadFragmentRefreshLayout.isRefreshing = false - binding.articleReadItemTextViewLike.text = postRank.getLike().toString() - } - Log("onAL", "get data from web over") - } catch (e: Exception) { - Log("onAL", "Error : $e") - runOnUI { - Toast.makeText( - activity, - "Error : $e", - Toast.LENGTH_SHORT - ) - .show() - binding.articleReadFragmentRefreshLayout.isRefreshing = false - } - } - GattingData = false - } - mThread = HandlerThread("name") - mThread!!.start() - mThreadHandler = Handler(mThread!!.looper) - mThreadHandler!!.post(r1) - } - private val haveApi = true - private fun putDefaultHeader() { - data.add( - ArticleReadAdapter.Item.HeaderItem( - articleTitle, - articleAuth, - articleTime, - articleClass, - articleBoard - ) - ) - } - - private var GattingData = false - private fun loadData() { - if (GattingData) return - data.clear() - putDefaultHeader() - mAdapter!!.notifyDataSetChanged() - dataFromApi + viewModel.loadData(board, fileName, aid, articleBoard) } private fun setRankMenu(view: View) { if (!(haveApi && useApi)) { return } - val id = currentActivity - .getSharedPreferences("MainSetting", Context.MODE_PRIVATE) - .getString("APIPTTID", "") + val id = preferences.getString("APIPTTID", "") if (id!!.isEmpty()) { loadFragment(LoginPageFragment.newInstance(), currentFragment) return @@ -269,98 +172,19 @@ class ArticleReadFragment : BaseFragment() { R.id.post_article_rank_dislike -> rank = PostRankMark.Dislike R.id.post_article_rank_non -> rank = PostRankMark.None } - setRank(rank) + progressDialog = ProgressDialog.show( + currentActivity, + "", + "Please wait." + ).apply { + window?.setBackgroundDrawableResource(R.drawable.dialog_background) + viewModel.setRank(board, aid, orgUrl, rank) + } true } popupMenu.show() } - private fun refreshRank() { - r1 = Runnable { - runOnUI { binding.articleReadFragmentRefreshLayout.isRefreshing = true } - GattingData = true - try { - val postRank = postRemoteDataSource.getPostRank(board, aid) - for (i in data.indices) { - val item = data[i] - if (item !is ArticleReadAdapter.Item.CenterBarItem) continue - data[i] = ArticleReadAdapter.Item.CenterBarItem(postRank.getLike().toString(), item.floor) - break - } - runOnUI { - binding.articleReadFragmentRefreshLayout.isRefreshing = false - mAdapter!!.notifyDataSetChanged() - binding.articleReadItemTextViewLike.text = postRank.getLike().toString() - } - Log("onAL", "get data from web over") - } catch (e: Exception) { - Log("onAL", "Error : $e") - runOnUI { - Toast.makeText( - currentActivity, - "Error : $e", - Toast.LENGTH_SHORT - ) - .show() - binding.articleReadFragmentRefreshLayout.isRefreshing = false - } - } - GattingData = false - } - mThread = HandlerThread("name") - mThread!!.start() - mThreadHandler = Handler(mThread!!.looper) - mThreadHandler!!.post(r1) - } - - private var mDialog: ProgressDialog? = null - private fun setRank(rank: PostRankMark) { - mDialog = ProgressDialog.show(currentActivity, "", "Please wait.").apply { - getWindow()!!.setBackgroundDrawableResource(R.drawable.dialog_background) - object : Thread() { - override fun run() { - try { - val id = currentActivity - .getSharedPreferences("MainSetting", Context.MODE_PRIVATE) - .getString("APIPTTID", "") - if (id.isNullOrEmpty()) { - throw Exception("No Ptt id") - } - val p = Pattern.compile( - "www.ptt.cc/bbs/([-a-zA-Z0-9_]{2,})/([M|G].[-a-zA-Z0-9._]{1,30}).htm" - ) - val m = p.matcher(orgUrl) - if (m.find()) { - val aid = AidConverter.urlToAid(orgUrl) - postRemoteDataSource.setPostRank( - aid.boardTitle, - aid.aid, - id, - rank - ) - } else { - throw Exception("error") - } - runOnUI { - dismiss() - refreshRank() - } - } catch (e: Exception) { - runOnUI { - dismiss() - Toast.makeText( - currentActivity, - "Error : $e", - Toast.LENGTH_SHORT - ) - .show() - } - } - } - }.start() - } - } - override fun onDestroyView() { super.onDestroyView() _binding = null @@ -373,16 +197,7 @@ class ArticleReadFragment : BaseFragment() { override fun onDestroy() { super.onDestroy() - postRemoteDataSource.disposeAll() - data.clear() - // 移除工作 - if (mThreadHandler != null) { - mThreadHandler!!.removeCallbacks(r1!!) - } - // (關閉Thread) - if (mThread != null) { - mThread!!.quit() - } + viewModel.data.clear() val typedValue = TypedValue() val theme = currentActivity.theme theme.resolveAttribute(R.attr.black, typedValue, true) diff --git a/app/src/main/java/tw/y_studio/ptt/ui/article/read/ArticleReadViewModel.kt b/app/src/main/java/tw/y_studio/ptt/ui/article/read/ArticleReadViewModel.kt new file mode 100644 index 0000000..34508e8 --- /dev/null +++ b/app/src/main/java/tw/y_studio/ptt/ui/article/read/ArticleReadViewModel.kt @@ -0,0 +1,215 @@ +package tw.y_studio.ptt.ui.article.read + +import android.content.SharedPreferences +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import tw.y_studio.ptt.api.PostRankMark +import tw.y_studio.ptt.ptt.AidConverter +import tw.y_studio.ptt.source.remote.post.IPostRemoteDataSource +import tw.y_studio.ptt.utils.Log +import tw.y_studio.ptt.utils.StringUtils +import java.util.regex.Pattern + +class ArticleReadViewModel( + private val postRemoteDataSource: IPostRemoteDataSource, + private val preferences: SharedPreferences, + private val ioDispatcher: CoroutineDispatcher +) : ViewModel() { + + val data: MutableList = mutableListOf() + + private var headerItem: ArticleReadAdapter.Item? = null + + private val _loadingState = MutableLiveData() + val loadingState: LiveData = _loadingState + + private val _progressDialogState = MutableLiveData() + val progressDialogState: LiveData = _progressDialogState + + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData = _errorMessage + + private val _likeNumber = MutableLiveData() + val likeNumber: LiveData = _likeNumber + + private var floorNum = 0 + + var originalArticleTitle = "" + + fun createDefaultHeader( + articleTitle: String, + articleAuth: String, + articleTime: String, + articleClass: String, + articleBoard: String + ) { + headerItem = ArticleReadAdapter.Item.HeaderItem( + articleTitle, + articleAuth, + articleTime, + articleClass, + articleBoard + ) + } + + fun putDefaultHeader() { + headerItem?.let { data.add(it) } + } + + private suspend fun dataFromApi( + board: String, + fileName: String, + aid: String, + articleBoard: String + ) = withContext(ioDispatcher) { + Log("onAR", "get data from web start") + val post = postRemoteDataSource.getPost(board, fileName) + val postRank = postRemoteDataSource.getPostRank(board, aid) + floorNum = post.comments.size + originalArticleTitle = post.title + data.add( + ArticleReadAdapter.Item.HeaderItem( + post.title, + "${post.auth} (${post.authNickName})", + post.date, + post.classString, + articleBoard + ) + ) + + val contents = post.content.split("\r\n".toRegex()).toTypedArray() + var contentTemp = StringBuilder() + for (i in contents.indices) { + val cmd = contents[i] + val urlM = StringUtils.UrlPattern.matcher(cmd) + if (urlM.find()) { + data.add(ArticleReadAdapter.Item.ContentLineItem(contentTemp.toString())) + contentTemp = StringBuilder() + data.add(ArticleReadAdapter.Item.ContentLineItem(cmd)) + val imageUrl = StringUtils.getImgUrl(cmd) + for (urlString in imageUrl) { + data.add(ArticleReadAdapter.Item.ImageItem(-2, urlString)) + } + } else { + contentTemp.append(cmd) + if (i < contents.size - 1) { + contentTemp.append("\n") + } + } + } + if (contentTemp.toString().isNotEmpty()) { + data.add(ArticleReadAdapter.Item.ContentLineItem(contentTemp.toString())) + } + data.add(ArticleReadAdapter.Item.CenterBarItem(postRank.getLike().toString(), floorNum.toString())) + post.comments.forEachIndexed { index, comment -> + data.add(ArticleReadAdapter.Item.CommentItem(index, comment.content, comment.userid)) + val imageUrl: List = StringUtils.getImgUrl(StringUtils.notNullString(comment.content)) + for (urlString in imageUrl) { + data.add(ArticleReadAdapter.Item.ImageItem(index, urlString)) + } + data.add( + ArticleReadAdapter.Item.CommentBarItem( + index, + comment.date, + "${index + 1}F", + "0" + ) + ) + } + postRank + } + + fun loadData( + board: String, + fileName: String, + aid: String, + articleBoard: String + ) = viewModelScope.launch { + if (_loadingState.value == true) return@launch + data.clear() + _loadingState.value = true + putDefaultHeader() + try { + val postRank = dataFromApi(board, fileName, aid, articleBoard) + _likeNumber.value = postRank.getLike().toString() + Log("onAL", "get data from web over") + } catch (e: Exception) { + Log("onAL", "Error : $e") + _errorMessage.value = "Error : $e" + } + + _loadingState.value = false + } + + fun setRank( + board: String, + aid: String, + orgUrl: String, + rank: PostRankMark + ) = viewModelScope.launch { + try { + loadRankData(board, aid, orgUrl, rank) + _progressDialogState.value = false + } catch (e: Exception) { + _progressDialogState.value = false + _errorMessage.value = "Error : $e" + } + } + + private suspend fun loadRankData( + board: String, + aid: String, + orgUrl: String, + rank: PostRankMark + ) = withContext(ioDispatcher) { + val id = preferences.getString("APIPTTID", "") + if (id.isNullOrEmpty()) { + throw Exception("No Ptt id") + } + val p = Pattern.compile( + "www.ptt.cc/bbs/([-a-zA-Z0-9_]{2,})/([M|G].[-a-zA-Z0-9._]{1,30}).htm" + ) + val m = p.matcher(orgUrl) + if (m.find()) { + val aidBean = AidConverter.urlToAid(orgUrl) + postRemoteDataSource.setPostRank( + aidBean.boardTitle, + aidBean.aid, + id, + rank + ) + refreshRank(board, aid) + } else { + throw Exception("error") + } + } + + private suspend fun refreshRank(board: String, aid: String) = withContext(ioDispatcher) { + _loadingState.value = true + try { + val postRank = postRemoteDataSource.getPostRank(board, aid) + for (i in data.indices) { + val item = data[i] + if (item !is ArticleReadAdapter.Item.CenterBarItem) continue + data[i] = ArticleReadAdapter.Item.CenterBarItem(postRank.getLike().toString(), item.floor) + break + } + _likeNumber.value = postRank.getLike().toString() + Log("onAL", "get data from web over") + } catch (e: Exception) { + Log("onAL", "Error : $e") + _errorMessage.value = "Error : $e" + } + _loadingState.value = false + } + + override fun onCleared() { + postRemoteDataSource.disposeAll() + super.onCleared() + } +}