검색 UI 추가
This commit is contained in:
		| @@ -35,8 +35,8 @@ android { | ||||
|         applicationId "kr.co.vividnext.sodalive" | ||||
|         minSdk 23 | ||||
|         targetSdk 34 | ||||
|         versionCode 154 | ||||
|         versionName "1.31.1" | ||||
|         versionCode 156 | ||||
|         versionName "1.32.0" | ||||
|     } | ||||
|  | ||||
|     buildTypes { | ||||
|   | ||||
| @@ -174,6 +174,8 @@ | ||||
|         <activity android:name=".audio_content.main.v2.series.origianl_audio_drama.OriginalAudioDramaContentAllActivity" /> | ||||
|         <activity android:name=".audio_content.main.v2.series.completed.CompletedSeriesActivity" /> | ||||
|  | ||||
|         <activity android:name=".search.SearchActivity" /> | ||||
|  | ||||
|         <activity android:name=".mypage.alarm.AlarmListActivity" /> | ||||
|         <activity android:name=".mypage.alarm.AddAlarmActivity" /> | ||||
|         <activity android:name=".mypage.alarm.select_audio_content.AlarmSelectAudioContentActivity" /> | ||||
|   | ||||
| @@ -46,6 +46,7 @@ import kr.co.vividnext.sodalive.extensions.dpToPx | ||||
| import kr.co.vividnext.sodalive.live.event_banner.EventBannerAdapter | ||||
| import kr.co.vividnext.sodalive.main.MainActivity | ||||
| import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity | ||||
| import kr.co.vividnext.sodalive.search.SearchActivity | ||||
| import kr.co.vividnext.sodalive.settings.event.EventDetailActivity | ||||
| import kr.co.vividnext.sodalive.settings.notice.NoticeDetailActivity | ||||
| import kr.co.vividnext.sodalive.settings.notification.MemberRole | ||||
| @@ -154,6 +155,12 @@ class AudioContentMainTabHomeFragment : BaseFragment<FragmentAudioContentMainTab | ||||
|         if (SharedPreferenceManager.token.isNotBlank()) { | ||||
|             binding.flSearch.visibility = View.VISIBLE | ||||
|             binding.flSearch.setOnClickListener { | ||||
|                 startActivity( | ||||
|                     Intent( | ||||
|                         requireContext(), | ||||
|                         SearchActivity::class.java | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|         } else { | ||||
|             binding.flSearch.visibility = View.GONE | ||||
|   | ||||
| @@ -137,6 +137,9 @@ import kr.co.vividnext.sodalive.mypage.service_center.ServiceCenterViewModel | ||||
| import kr.co.vividnext.sodalive.network.TokenAuthenticator | ||||
| import kr.co.vividnext.sodalive.report.ReportApi | ||||
| import kr.co.vividnext.sodalive.report.ReportRepository | ||||
| import kr.co.vividnext.sodalive.search.SearchApi | ||||
| import kr.co.vividnext.sodalive.search.SearchRepository | ||||
| import kr.co.vividnext.sodalive.search.SearchViewModel | ||||
| import kr.co.vividnext.sodalive.settings.ContentSettingsViewModel | ||||
| import kr.co.vividnext.sodalive.settings.SettingsViewModel | ||||
| import kr.co.vividnext.sodalive.settings.event.EventApi | ||||
| @@ -229,6 +232,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { | ||||
|         single { ApiBuilder().build(get(), PlaylistApi::class.java) } | ||||
|         single { ApiBuilder().build(get(), AuditionApi::class.java) } | ||||
|         single { ApiBuilder().build(get(), AdTrackingApi::class.java) } | ||||
|         single { ApiBuilder().build(get(), SearchApi::class.java) } | ||||
|     } | ||||
|  | ||||
|     private val viewModelModule = module { | ||||
| @@ -323,6 +327,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { | ||||
|         viewModel { IntroduceCreatorViewModel(get()) } | ||||
|         viewModel { CompletedSeriesViewModel(get()) } | ||||
|         viewModel { AlarmContentAllViewModel(get()) } | ||||
|         viewModel { SearchViewModel(get()) } | ||||
|     } | ||||
|  | ||||
|     private val repositoryModule = module { | ||||
| @@ -363,6 +368,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { | ||||
|         factory { AudioContentMainTabFreeRepository(get()) } | ||||
|         factory { OriginalAudioDramaContentAllRepository(get()) } | ||||
|         factory { AdTrackingRepository(get()) } | ||||
|         factory { SearchRepository(get()) } | ||||
|     } | ||||
|  | ||||
|     private val moduleList = listOf( | ||||
|   | ||||
| @@ -0,0 +1,556 @@ | ||||
| package kr.co.vividnext.sodalive.search | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.app.Service | ||||
| import android.content.Intent | ||||
| import android.graphics.Rect | ||||
| import android.os.Bundle | ||||
| import android.os.Handler | ||||
| import android.os.Looper | ||||
| import android.view.View | ||||
| import android.view.inputmethod.InputMethodManager | ||||
| import android.widget.Toast | ||||
| import androidx.annotation.OptIn | ||||
| import androidx.media3.common.util.UnstableApi | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import com.google.android.material.tabs.TabLayout | ||||
| import com.jakewharton.rxbinding4.widget.textChanges | ||||
| import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers | ||||
| import io.reactivex.rxjava3.schedulers.Schedulers | ||||
| import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity | ||||
| import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity | ||||
| import kr.co.vividnext.sodalive.base.BaseActivity | ||||
| import kr.co.vividnext.sodalive.common.Constants | ||||
| import kr.co.vividnext.sodalive.common.LoadingDialog | ||||
| import kr.co.vividnext.sodalive.databinding.ActivitySearchBinding | ||||
| import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity | ||||
| import kr.co.vividnext.sodalive.extensions.dpToPx | ||||
| import org.koin.android.ext.android.inject | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| class SearchActivity : BaseActivity<ActivitySearchBinding>(ActivitySearchBinding::inflate) { | ||||
|  | ||||
|     private val viewModel: SearchViewModel by inject() | ||||
|  | ||||
|     private lateinit var imm: InputMethodManager | ||||
|     private lateinit var loadingDialog: LoadingDialog | ||||
|  | ||||
|     private val handler = Handler(Looper.getMainLooper()) | ||||
|  | ||||
|     private lateinit var unifiedChannelAdapter: SearchAdapter | ||||
|     private lateinit var unifiedContentAdapter: SearchAdapter | ||||
|     private lateinit var unifiedSeriesAdapter: SearchAdapter | ||||
|  | ||||
|     private lateinit var channelAdapter: SearchAdapter | ||||
|     private lateinit var contentAdapter: SearchAdapter | ||||
|     private lateinit var seriesAdapter: SearchAdapter | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         imm = getSystemService( | ||||
|             Service.INPUT_METHOD_SERVICE | ||||
|         ) as InputMethodManager | ||||
|  | ||||
|         binding.etSearch.requestFocus() | ||||
|         handler.postDelayed({ | ||||
|             val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager | ||||
|             imm.showSoftInput(binding.etSearch, InputMethodManager.SHOW_IMPLICIT) | ||||
|         }, 500) | ||||
|  | ||||
|         bindData() | ||||
|     } | ||||
|  | ||||
|     override fun onPause() { | ||||
|         hideKeyboard() | ||||
|         super.onPause() | ||||
|     } | ||||
|  | ||||
|     override fun setupView() { | ||||
|         loadingDialog = LoadingDialog(this, layoutInflater) | ||||
|         binding.ivBack.setOnClickListener { finish() } | ||||
|  | ||||
|         setupTabs() | ||||
|         setupUnifiedView() | ||||
|         setupChannelListView() | ||||
|         setupContentListView() | ||||
|         setupSeriesListView() | ||||
|     } | ||||
|  | ||||
|     private fun setupChannelListView() { | ||||
|         channelAdapter = SearchAdapter { clickItem(it) } | ||||
|  | ||||
|         binding.rvCreator.layoutManager = LinearLayoutManager( | ||||
|             this, | ||||
|             LinearLayoutManager.VERTICAL, | ||||
|             false | ||||
|         ) | ||||
|  | ||||
|         binding.rvCreator.addItemDecoration(object : RecyclerView.ItemDecoration() { | ||||
|             override fun getItemOffsets( | ||||
|                 outRect: Rect, | ||||
|                 view: View, | ||||
|                 parent: RecyclerView, | ||||
|                 state: RecyclerView.State | ||||
|             ) { | ||||
|                 super.getItemOffsets(outRect, view, parent, state) | ||||
|  | ||||
|                 outRect.left = 13.3f.dpToPx().toInt() | ||||
|                 outRect.right = 13.3f.dpToPx().toInt() | ||||
|                 outRect.top = 13.3f.dpToPx().toInt() | ||||
|                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         binding.rvCreator.addOnScrollListener(object : RecyclerView.OnScrollListener() { | ||||
|             override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | ||||
|                 super.onScrolled(recyclerView, dx, dy) | ||||
|  | ||||
|                 val layoutManager = recyclerView.layoutManager as? LinearLayoutManager | ||||
|                 if ( | ||||
|                     layoutManager != null && | ||||
|                     layoutManager | ||||
|                         .findLastCompletelyVisibleItemPosition() == channelAdapter.itemCount - 1 | ||||
|                 ) { | ||||
|                     viewModel.searchCreatorList() | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         binding.rvCreator.adapter = channelAdapter | ||||
|     } | ||||
|  | ||||
|     private fun setupContentListView() { | ||||
|         contentAdapter = SearchAdapter { clickItem(it) } | ||||
|  | ||||
|         binding.rvContent.layoutManager = LinearLayoutManager( | ||||
|             this, | ||||
|             LinearLayoutManager.VERTICAL, | ||||
|             false | ||||
|         ) | ||||
|  | ||||
|         binding.rvContent.addItemDecoration(object : RecyclerView.ItemDecoration() { | ||||
|             override fun getItemOffsets( | ||||
|                 outRect: Rect, | ||||
|                 view: View, | ||||
|                 parent: RecyclerView, | ||||
|                 state: RecyclerView.State | ||||
|             ) { | ||||
|                 super.getItemOffsets(outRect, view, parent, state) | ||||
|  | ||||
|                 outRect.left = 13.3f.dpToPx().toInt() | ||||
|                 outRect.right = 13.3f.dpToPx().toInt() | ||||
|                 outRect.top = 13.3f.dpToPx().toInt() | ||||
|                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() { | ||||
|             override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | ||||
|                 super.onScrolled(recyclerView, dx, dy) | ||||
|  | ||||
|                 val layoutManager = recyclerView.layoutManager as? LinearLayoutManager | ||||
|                 if ( | ||||
|                     layoutManager != null && | ||||
|                     layoutManager | ||||
|                         .findLastCompletelyVisibleItemPosition() == contentAdapter.itemCount - 1 | ||||
|                 ) { | ||||
|                     viewModel.searchContentList() | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         binding.rvContent.adapter = contentAdapter | ||||
|     } | ||||
|  | ||||
|     private fun setupSeriesListView() { | ||||
|         seriesAdapter = SearchAdapter { clickItem(it) } | ||||
|  | ||||
|         binding.rvSeries.layoutManager = LinearLayoutManager( | ||||
|             this, | ||||
|             LinearLayoutManager.VERTICAL, | ||||
|             false | ||||
|         ) | ||||
|  | ||||
|         binding.rvSeries.addItemDecoration(object : RecyclerView.ItemDecoration() { | ||||
|             override fun getItemOffsets( | ||||
|                 outRect: Rect, | ||||
|                 view: View, | ||||
|                 parent: RecyclerView, | ||||
|                 state: RecyclerView.State | ||||
|             ) { | ||||
|                 super.getItemOffsets(outRect, view, parent, state) | ||||
|  | ||||
|                 outRect.left = 13.3f.dpToPx().toInt() | ||||
|                 outRect.right = 13.3f.dpToPx().toInt() | ||||
|                 outRect.top = 13.3f.dpToPx().toInt() | ||||
|                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         binding.rvSeries.addOnScrollListener(object : RecyclerView.OnScrollListener() { | ||||
|             override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | ||||
|                 super.onScrolled(recyclerView, dx, dy) | ||||
|  | ||||
|                 val layoutManager = recyclerView.layoutManager as? LinearLayoutManager | ||||
|                 if ( | ||||
|                     layoutManager != null && | ||||
|                     layoutManager | ||||
|                         .findLastCompletelyVisibleItemPosition() == seriesAdapter.itemCount - 1 | ||||
|                 ) { | ||||
|                     viewModel.searchSeriesList() | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         binding.rvSeries.adapter = seriesAdapter | ||||
|     } | ||||
|  | ||||
|     private fun setupUnifiedView() { | ||||
|         unifiedChannelAdapter = SearchAdapter { clickItem(it) } | ||||
|         unifiedContentAdapter = SearchAdapter { clickItem(it) } | ||||
|         unifiedSeriesAdapter = SearchAdapter { clickItem(it) } | ||||
|  | ||||
|         binding.tvMoreCreator.setOnClickListener { | ||||
|             viewModel.changeTab(SearchViewModel.SearchPageTab.CREATOR) | ||||
|         } | ||||
|  | ||||
|         binding.tvMoreContent.setOnClickListener { | ||||
|             viewModel.changeTab(SearchViewModel.SearchPageTab.CONTENT) | ||||
|         } | ||||
|  | ||||
|         binding.tvMoreSeries.setOnClickListener { | ||||
|             viewModel.changeTab(SearchViewModel.SearchPageTab.SERIES) | ||||
|         } | ||||
|  | ||||
|         binding.rvUnifiedCreator.layoutManager = LinearLayoutManager( | ||||
|             this, | ||||
|             LinearLayoutManager.VERTICAL, | ||||
|             false | ||||
|         ) | ||||
|         binding.rvUnifiedCreator.addItemDecoration(object : RecyclerView.ItemDecoration() { | ||||
|             override fun getItemOffsets( | ||||
|                 outRect: Rect, | ||||
|                 view: View, | ||||
|                 parent: RecyclerView, | ||||
|                 state: RecyclerView.State | ||||
|             ) { | ||||
|                 super.getItemOffsets(outRect, view, parent, state) | ||||
|  | ||||
|                 outRect.left = 13.3f.dpToPx().toInt() | ||||
|                 outRect.right = 13.3f.dpToPx().toInt() | ||||
|                 outRect.top = 13.3f.dpToPx().toInt() | ||||
|                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||
|             } | ||||
|         }) | ||||
|         binding.rvUnifiedCreator.adapter = unifiedChannelAdapter | ||||
|  | ||||
|         binding.rvUnifiedContent.layoutManager = LinearLayoutManager( | ||||
|             this, | ||||
|             LinearLayoutManager.VERTICAL, | ||||
|             false | ||||
|         ) | ||||
|         binding.rvUnifiedContent.addItemDecoration(object : RecyclerView.ItemDecoration() { | ||||
|             override fun getItemOffsets( | ||||
|                 outRect: Rect, | ||||
|                 view: View, | ||||
|                 parent: RecyclerView, | ||||
|                 state: RecyclerView.State | ||||
|             ) { | ||||
|                 super.getItemOffsets(outRect, view, parent, state) | ||||
|  | ||||
|                 outRect.left = 13.3f.dpToPx().toInt() | ||||
|                 outRect.right = 13.3f.dpToPx().toInt() | ||||
|                 outRect.top = 13.3f.dpToPx().toInt() | ||||
|                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||
|             } | ||||
|         }) | ||||
|         binding.rvUnifiedContent.adapter = unifiedContentAdapter | ||||
|  | ||||
|         binding.rvUnifiedSeries.layoutManager = LinearLayoutManager( | ||||
|             this, | ||||
|             LinearLayoutManager.VERTICAL, | ||||
|             false | ||||
|         ) | ||||
|         binding.rvUnifiedSeries.addItemDecoration(object : RecyclerView.ItemDecoration() { | ||||
|             override fun getItemOffsets( | ||||
|                 outRect: Rect, | ||||
|                 view: View, | ||||
|                 parent: RecyclerView, | ||||
|                 state: RecyclerView.State | ||||
|             ) { | ||||
|                 super.getItemOffsets(outRect, view, parent, state) | ||||
|  | ||||
|                 outRect.left = 13.3f.dpToPx().toInt() | ||||
|                 outRect.right = 13.3f.dpToPx().toInt() | ||||
|                 outRect.top = 13.3f.dpToPx().toInt() | ||||
|                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||
|             } | ||||
|         }) | ||||
|         binding.rvUnifiedSeries.adapter = unifiedSeriesAdapter | ||||
|     } | ||||
|  | ||||
|     @OptIn(UnstableApi::class) | ||||
|     private fun clickItem(item: SearchResponseItem) { | ||||
|         hideKeyboard() | ||||
|  | ||||
|         startActivity( | ||||
|             when (item.type) { | ||||
|                 SearchResponseType.CREATOR -> { | ||||
|                     Intent(applicationContext, UserProfileActivity::class.java).apply { | ||||
|                         putExtra(Constants.EXTRA_USER_ID, item.id) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 SearchResponseType.CONTENT -> { | ||||
|                     Intent(applicationContext, AudioContentDetailActivity::class.java).apply { | ||||
|                         putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, item.id) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 SearchResponseType.SERIES -> { | ||||
|                     Intent(applicationContext, SeriesDetailActivity::class.java).apply { | ||||
|                         putExtra(Constants.EXTRA_SERIES_ID, item.id) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     private fun setupTabs() { | ||||
|         val tabs = binding.tabs | ||||
|         val tabTitles = listOf("통합", "채널", "콘텐츠", "시리즈") | ||||
|         for (title in tabTitles) { | ||||
|             tabs.addTab(tabs.newTab().setText(title)) | ||||
|         } | ||||
|  | ||||
|         tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { | ||||
|             override fun onTabSelected(tab: TabLayout.Tab) { | ||||
|                 val selectedTab = SearchViewModel.SearchPageTab.fromOrdinal(tab.position) | ||||
|                 viewModel.changeTab(selectedTab!!) | ||||
|  | ||||
|                 tab.view.isSelected = true | ||||
|                 hideKeyboard() | ||||
|             } | ||||
|  | ||||
|             override fun onTabUnselected(tab: TabLayout.Tab) { | ||||
|                 tab.view.isSelected = false | ||||
|             } | ||||
|  | ||||
|             override fun onTabReselected(tab: TabLayout.Tab) { | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     @SuppressLint("NotifyDataSetChanged") | ||||
|     private fun bindData() { | ||||
|         compositeDisposable.add( | ||||
|             binding.etSearch.textChanges().skip(1) | ||||
|                 .debounce(500, TimeUnit.MILLISECONDS) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .subscribe { | ||||
|                     channelAdapter.clear() | ||||
|                     contentAdapter.clear() | ||||
|                     seriesAdapter.clear() | ||||
|  | ||||
|                     unifiedChannelAdapter.clear() | ||||
|                     unifiedContentAdapter.clear() | ||||
|                     unifiedSeriesAdapter.clear() | ||||
|  | ||||
|                     viewModel.keyword = it.toString() | ||||
|                     if (it.length >= 2) { | ||||
|                         viewModel.searchUnified() | ||||
|                         binding.tabs.visibility = View.VISIBLE | ||||
|                         binding.tvResultX.visibility = View.GONE | ||||
|                     } else { | ||||
|                         binding.nsSearchUnified.visibility = View.GONE | ||||
|                         binding.rvCreator.visibility = View.GONE | ||||
|                         binding.rvContent.visibility = View.GONE | ||||
|                         binding.rvSeries.visibility = View.GONE | ||||
|                         binding.tabs.visibility = View.GONE | ||||
|                         binding.tvResultX.visibility = View.GONE | ||||
|                     } | ||||
|                 } | ||||
|         ) | ||||
|  | ||||
|         viewModel.toastLiveData.observe(this) { | ||||
|             it?.let { Toast.makeText(this@SearchActivity, it, Toast.LENGTH_LONG).show() } | ||||
|         } | ||||
|  | ||||
|         viewModel.isLoading.observe(this) { | ||||
|             if (it) { | ||||
|                 loadingDialog.show(screenWidth, "") | ||||
|             } else { | ||||
|                 loadingDialog.dismiss() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         viewModel.searchUnifiedLiveData.observe(this) { | ||||
|             if ( | ||||
|                 it.creatorList.isEmpty() && | ||||
|                 it.seriesList.isEmpty() && | ||||
|                 it.contentList.isEmpty() | ||||
|             ) { | ||||
|                 binding.tabs.visibility = View.GONE | ||||
|                 hideAllView() | ||||
|                 if (viewModel.keyword.isNotBlank()) { | ||||
|                     binding.tvResultX.visibility = View.VISIBLE | ||||
|                 } else { | ||||
|                     binding.tvResultX.visibility = View.GONE | ||||
|                 } | ||||
|             } else { | ||||
|                 binding.tabs.visibility = View.VISIBLE | ||||
|                 binding.nsSearchUnified.visibility = View.VISIBLE | ||||
|  | ||||
|                 if (it.creatorList.isNotEmpty()) { | ||||
|                     binding.tvCreatorTitle.visibility = View.VISIBLE | ||||
|                     binding.tvMoreCreator.visibility = View.VISIBLE | ||||
|                     binding.rvUnifiedCreator.visibility = View.VISIBLE | ||||
|                     unifiedChannelAdapter.items.addAll(it.creatorList) | ||||
|                     unifiedChannelAdapter.notifyDataSetChanged() | ||||
|                 } else { | ||||
|                     binding.tvCreatorTitle.visibility = View.GONE | ||||
|                     binding.tvMoreCreator.visibility = View.GONE | ||||
|                     binding.rvUnifiedCreator.visibility = View.GONE | ||||
|                 } | ||||
|  | ||||
|                 if (it.contentList.isNotEmpty()) { | ||||
|                     binding.tvContentTitle.visibility = View.VISIBLE | ||||
|                     binding.tvMoreContent.visibility = View.VISIBLE | ||||
|                     binding.rvUnifiedContent.visibility = View.VISIBLE | ||||
|                     unifiedContentAdapter.items.addAll(it.contentList) | ||||
|                     unifiedContentAdapter.notifyDataSetChanged() | ||||
|                 } else { | ||||
|                     binding.tvContentTitle.visibility = View.GONE | ||||
|                     binding.tvMoreContent.visibility = View.GONE | ||||
|                     binding.rvUnifiedContent.visibility = View.GONE | ||||
|                 } | ||||
|  | ||||
|                 if (it.seriesList.isNotEmpty()) { | ||||
|                     binding.tvSeriesTitle.visibility = View.VISIBLE | ||||
|                     binding.tvMoreSeries.visibility = View.VISIBLE | ||||
|                     binding.rvUnifiedSeries.visibility = View.VISIBLE | ||||
|                     unifiedSeriesAdapter.items.addAll(it.seriesList) | ||||
|                     unifiedSeriesAdapter.notifyDataSetChanged() | ||||
|                 } else { | ||||
|                     binding.tvSeriesTitle.visibility = View.GONE | ||||
|                     binding.tvMoreSeries.visibility = View.GONE | ||||
|                     binding.rvUnifiedSeries.visibility = View.GONE | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         viewModel.searchCreatorLiveData.observe(this) { | ||||
|             channelAdapter.items.addAll(it) | ||||
|             channelAdapter.notifyDataSetChanged() | ||||
|  | ||||
|             hideAllView() | ||||
|             if (channelAdapter.items.isEmpty()) { | ||||
|                 if (viewModel.keyword.isNotBlank()) { | ||||
|                     binding.tvResultX.visibility = View.VISIBLE | ||||
|                 } else { | ||||
|                     binding.tvResultX.visibility = View.GONE | ||||
|                 } | ||||
|             } else { | ||||
|                 binding.rvCreator.visibility = View.VISIBLE | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         viewModel.searchContentLiveData.observe(this) { | ||||
|             contentAdapter.items.addAll(it) | ||||
|             contentAdapter.notifyDataSetChanged() | ||||
|  | ||||
|             hideAllView() | ||||
|             if (contentAdapter.items.isEmpty()) { | ||||
|                 if (viewModel.keyword.isNotBlank()) { | ||||
|                     binding.tvResultX.visibility = View.VISIBLE | ||||
|                 } else { | ||||
|                     binding.tvResultX.visibility = View.GONE | ||||
|                 } | ||||
|             } else { | ||||
|                 binding.rvContent.visibility = View.VISIBLE | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         viewModel.searchSeriesLiveData.observe(this) { | ||||
|             seriesAdapter.items.addAll(it) | ||||
|             seriesAdapter.notifyDataSetChanged() | ||||
|  | ||||
|             hideAllView() | ||||
|             if (seriesAdapter.items.isEmpty()) { | ||||
|                 if (viewModel.keyword.isNotBlank()) { | ||||
|                     binding.tvResultX.visibility = View.VISIBLE | ||||
|                 } else { | ||||
|                     binding.tvResultX.visibility = View.GONE | ||||
|                 } | ||||
|             } else { | ||||
|                 binding.rvSeries.visibility = View.VISIBLE | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         viewModel.currentTabLiveData.observe(this) { currentTab -> | ||||
|             hideAllView() | ||||
|  | ||||
|             binding.tabs.getTabAt(currentTab.ordinal)?.select() | ||||
|             when (currentTab) { | ||||
|                 SearchViewModel.SearchPageTab.CREATOR -> { | ||||
|                     if (channelAdapter.items.isEmpty()) { | ||||
|                         viewModel.searchCreatorList() | ||||
|                     } else { | ||||
|                         binding.rvCreator.visibility = View.VISIBLE | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 SearchViewModel.SearchPageTab.CONTENT -> { | ||||
|                     if (contentAdapter.items.isEmpty()) { | ||||
|                         viewModel.searchContentList() | ||||
|                     } else { | ||||
|                         binding.rvContent.visibility = View.VISIBLE | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 SearchViewModel.SearchPageTab.SERIES -> { | ||||
|                     if (seriesAdapter.items.isEmpty()) { | ||||
|                         viewModel.searchSeriesList() | ||||
|                     } else { | ||||
|                         binding.rvSeries.visibility = View.VISIBLE | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 else -> { | ||||
|                     if ( | ||||
|                         unifiedChannelAdapter.items.isEmpty() && | ||||
|                         unifiedContentAdapter.items.isEmpty() && | ||||
|                         unifiedSeriesAdapter.items.isEmpty() | ||||
|                     ) { | ||||
|                         if (viewModel.keyword.isNotBlank()) { | ||||
|                             binding.tvResultX.visibility = View.VISIBLE | ||||
|                         } else { | ||||
|                             binding.tvResultX.visibility = View.GONE | ||||
|                         } | ||||
|                     } else { | ||||
|                         binding.nsSearchUnified.visibility = View.VISIBLE | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun hideAllView() { | ||||
|         binding.nsSearchUnified.visibility = View.GONE | ||||
|         binding.rvCreator.visibility = View.GONE | ||||
|         binding.rvContent.visibility = View.GONE | ||||
|         binding.rvSeries.visibility = View.GONE | ||||
|         binding.tvResultX.visibility = View.GONE | ||||
|     } | ||||
|  | ||||
|     private fun hideKeyboard() { | ||||
|         handler.postDelayed({ | ||||
|             imm.hideSoftInputFromWindow( | ||||
|                 window.decorView.applicationWindowToken, | ||||
|                 InputMethodManager.HIDE_NOT_ALWAYS | ||||
|             ) | ||||
|         }, 100) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,97 @@ | ||||
| package kr.co.vividnext.sodalive.search | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import coil.load | ||||
| import coil.transform.CircleCropTransformation | ||||
| import coil.transform.RoundedCornersTransformation | ||||
| import kr.co.vividnext.sodalive.R | ||||
| import kr.co.vividnext.sodalive.databinding.ItemSearchBinding | ||||
| import kr.co.vividnext.sodalive.extensions.dpToPx | ||||
|  | ||||
| class SearchAdapter( | ||||
|     private val onClickItem: (SearchResponseItem) -> Unit | ||||
| ) : RecyclerView.Adapter<SearchAdapter.ViewHolder>() { | ||||
|     inner class ViewHolder( | ||||
|         private val context: Context, | ||||
|         private val binding: ItemSearchBinding | ||||
|     ) : RecyclerView.ViewHolder(binding.root) { | ||||
|  | ||||
|         @SuppressLint("CheckResult") | ||||
|         fun bind(item: SearchResponseItem) { | ||||
|             binding.ivProfile.visibility = View.GONE | ||||
|             binding.ivContent.visibility = View.GONE | ||||
|             binding.ivSeries.visibility = View.GONE | ||||
|             binding.tvNickname.visibility = View.GONE | ||||
|  | ||||
|             when (item.type) { | ||||
|                 SearchResponseType.CREATOR -> { | ||||
|                     binding.ivProfile.visibility = View.VISIBLE | ||||
|                     binding.ivProfile.load(item.imageUrl) { | ||||
|                         crossfade(true) | ||||
|                         placeholder(R.drawable.bg_placeholder) | ||||
|                         transformations(CircleCropTransformation()) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 SearchResponseType.CONTENT -> { | ||||
|                     binding.ivContent.visibility = View.VISIBLE | ||||
|                     binding.ivContent.load(item.imageUrl) { | ||||
|                         crossfade(true) | ||||
|                         placeholder(R.drawable.bg_placeholder) | ||||
|                         transformations(RoundedCornersTransformation(5.3f.dpToPx())) | ||||
|                     } | ||||
|                     binding.tvNickname.visibility = View.VISIBLE | ||||
|                 } | ||||
|  | ||||
|                 SearchResponseType.SERIES -> { | ||||
|                     binding.ivSeries.visibility = View.VISIBLE | ||||
|                     binding.ivSeries.load(item.imageUrl) { | ||||
|                         crossfade(true) | ||||
|                         placeholder(R.drawable.bg_placeholder) | ||||
|                         transformations(RoundedCornersTransformation(5.3f.dpToPx())) | ||||
|                     } | ||||
|  | ||||
|                     binding.ivSeries.load(item.imageUrl) { | ||||
|                         crossfade(true) | ||||
|                         placeholder(R.drawable.bg_placeholder) | ||||
|                         transformations(RoundedCornersTransformation(5.3f.dpToPx())) | ||||
|                     } | ||||
|                     binding.tvNickname.visibility = View.VISIBLE | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             binding.tvTitle.text = item.title | ||||
|             binding.tvNickname.text = item.nickname | ||||
|  | ||||
|             binding.root.setOnClickListener { onClickItem(item) } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     val items: MutableList<SearchResponseItem> = mutableListOf() | ||||
|  | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( | ||||
|         parent.context, | ||||
|         ItemSearchBinding.inflate( | ||||
|             LayoutInflater.from(parent.context), | ||||
|             parent, | ||||
|             false | ||||
|         ) | ||||
|     ) | ||||
|  | ||||
|     override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||||
|         holder.bind(items[position]) | ||||
|     } | ||||
|  | ||||
|     override fun getItemCount() = items.size | ||||
|  | ||||
|     @SuppressLint("NotifyDataSetChanged") | ||||
|     fun clear() { | ||||
|         items.clear() | ||||
|         notifyDataSetChanged() | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,48 @@ | ||||
| package kr.co.vividnext.sodalive.search | ||||
|  | ||||
| import io.reactivex.rxjava3.core.Single | ||||
| import kr.co.vividnext.sodalive.common.ApiResponse | ||||
| import kr.co.vividnext.sodalive.settings.ContentType | ||||
| import retrofit2.http.GET | ||||
| import retrofit2.http.Header | ||||
| import retrofit2.http.Query | ||||
|  | ||||
| interface SearchApi { | ||||
|     @GET("/search") | ||||
|     fun searchUnified( | ||||
|         @Query("keyword") keyword: String, | ||||
|         @Query("isAdultContentVisible") isAdultContentVisible: Boolean, | ||||
|         @Query("contentType") contentType: ContentType, | ||||
|         @Header("Authorization") authHeader: String | ||||
|     ): Single<ApiResponse<SearchUnifiedResponse>> | ||||
|  | ||||
|     @GET("/search/creators") | ||||
|     fun searchCreatorList( | ||||
|         @Query("keyword") keyword: String, | ||||
|         @Query("isAdultContentVisible") isAdultContentVisible: Boolean, | ||||
|         @Query("contentType") contentType: ContentType, | ||||
|         @Query("page") page: Int, | ||||
|         @Query("size") size: Int, | ||||
|         @Header("Authorization") authHeader: String | ||||
|     ): Single<ApiResponse<SearchResponse>> | ||||
|  | ||||
|     @GET("/search/contents") | ||||
|     fun searchContentList( | ||||
|         @Query("keyword") keyword: String, | ||||
|         @Query("isAdultContentVisible") isAdultContentVisible: Boolean, | ||||
|         @Query("contentType") contentType: ContentType, | ||||
|         @Query("page") page: Int, | ||||
|         @Query("size") size: Int, | ||||
|         @Header("Authorization") authHeader: String | ||||
|     ): Single<ApiResponse<SearchResponse>> | ||||
|  | ||||
|     @GET("/search/series") | ||||
|     fun searchSeriesList( | ||||
|         @Query("keyword") keyword: String, | ||||
|         @Query("isAdultContentVisible") isAdultContentVisible: Boolean, | ||||
|         @Query("contentType") contentType: ContentType, | ||||
|         @Query("page") page: Int, | ||||
|         @Query("size") size: Int, | ||||
|         @Header("Authorization") authHeader: String | ||||
|     ): Single<ApiResponse<SearchResponse>> | ||||
| } | ||||
| @@ -0,0 +1,58 @@ | ||||
| package kr.co.vividnext.sodalive.search | ||||
|  | ||||
| import kr.co.vividnext.sodalive.common.SharedPreferenceManager | ||||
| import kr.co.vividnext.sodalive.settings.ContentType | ||||
|  | ||||
| class SearchRepository(private val api: SearchApi) { | ||||
|     fun searchUnified( | ||||
|         keyword: String, | ||||
|         token: String | ||||
|     ) = api.searchUnified( | ||||
|         keyword = keyword, | ||||
|         isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible, | ||||
|         contentType = ContentType.values()[SharedPreferenceManager.contentPreference], | ||||
|         authHeader = token | ||||
|     ) | ||||
|  | ||||
|     fun searchCreatorList( | ||||
|         keyword: String, | ||||
|         page: Int, | ||||
|         size: Int, | ||||
|         token: String | ||||
|     ) = api.searchCreatorList( | ||||
|         keyword = keyword, | ||||
|         isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible, | ||||
|         contentType = ContentType.values()[SharedPreferenceManager.contentPreference], | ||||
|         page = page - 1, | ||||
|         size = size, | ||||
|         authHeader = token | ||||
|     ) | ||||
|  | ||||
|     fun searchContentList( | ||||
|         keyword: String, | ||||
|         page: Int, | ||||
|         size: Int, | ||||
|         token: String | ||||
|     ) = api.searchContentList( | ||||
|         keyword = keyword, | ||||
|         isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible, | ||||
|         contentType = ContentType.values()[SharedPreferenceManager.contentPreference], | ||||
|         page = page - 1, | ||||
|         size = size, | ||||
|         authHeader = token | ||||
|     ) | ||||
|  | ||||
|     fun searchSeriesList( | ||||
|         keyword: String, | ||||
|         page: Int, | ||||
|         size: Int, | ||||
|         token: String | ||||
|     ) = api.searchSeriesList( | ||||
|         keyword = keyword, | ||||
|         isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible, | ||||
|         contentType = ContentType.values()[SharedPreferenceManager.contentPreference], | ||||
|         page = page - 1, | ||||
|         size = size, | ||||
|         authHeader = token | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,33 @@ | ||||
| package kr.co.vividnext.sodalive.search | ||||
|  | ||||
| import androidx.annotation.Keep | ||||
| import com.google.gson.annotations.SerializedName | ||||
|  | ||||
| @Keep | ||||
| data class SearchUnifiedResponse( | ||||
|     @SerializedName("creatorList") val creatorList: List<SearchResponseItem>, | ||||
|     @SerializedName("contentList") val contentList: List<SearchResponseItem>, | ||||
|     @SerializedName("seriesList") val seriesList: List<SearchResponseItem> | ||||
| ) | ||||
|  | ||||
| @Keep | ||||
| data class SearchResponse( | ||||
|     @SerializedName("totalCount") val totalCount: Int, | ||||
|     @SerializedName("items") val items: List<SearchResponseItem> | ||||
| ) | ||||
|  | ||||
| @Keep | ||||
| data class SearchResponseItem( | ||||
|     @SerializedName("id") val id: Long, | ||||
|     @SerializedName("imageUrl") val imageUrl: String, | ||||
|     @SerializedName("title") val title: String, | ||||
|     @SerializedName("nickname") val nickname: String, | ||||
|     @SerializedName("type") val type: SearchResponseType | ||||
| ) | ||||
|  | ||||
| @Keep | ||||
| enum class SearchResponseType { | ||||
|     @SerializedName("CREATOR") CREATOR, | ||||
|     @SerializedName("CONTENT") CONTENT, | ||||
|     @SerializedName("SERIES") SERIES | ||||
| } | ||||
| @@ -0,0 +1,250 @@ | ||||
| package kr.co.vividnext.sodalive.search | ||||
|  | ||||
| import androidx.lifecycle.LiveData | ||||
| import androidx.lifecycle.MutableLiveData | ||||
| import com.orhanobut.logger.Logger | ||||
| import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers | ||||
| import io.reactivex.rxjava3.schedulers.Schedulers | ||||
| import kr.co.vividnext.sodalive.base.BaseViewModel | ||||
| import kr.co.vividnext.sodalive.common.SharedPreferenceManager | ||||
|  | ||||
| class SearchViewModel( | ||||
|     private val repository: SearchRepository | ||||
| ) : BaseViewModel() { | ||||
|     enum class SearchPageTab { | ||||
|         UNIFIED, CREATOR, CONTENT, SERIES; | ||||
|  | ||||
|         companion object { | ||||
|             fun fromOrdinal(ordinal: Int): SearchPageTab? { | ||||
|                 return SearchPageTab.values().getOrNull(ordinal) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     var keyword = "" | ||||
|  | ||||
|     private val _currentTabLiveData = MutableLiveData(SearchPageTab.UNIFIED) | ||||
|     val currentTabLiveData: LiveData<SearchPageTab> | ||||
|         get() = _currentTabLiveData | ||||
|  | ||||
|     private val _toastLiveData = MutableLiveData<String?>() | ||||
|     val toastLiveData: LiveData<String?> | ||||
|         get() = _toastLiveData | ||||
|  | ||||
|     private var _isLoading = MutableLiveData(false) | ||||
|     val isLoading: LiveData<Boolean> | ||||
|         get() = _isLoading | ||||
|  | ||||
|     private var _searchUnifiedLiveData = MutableLiveData<SearchUnifiedResponse>() | ||||
|     val searchUnifiedLiveData: LiveData<SearchUnifiedResponse> | ||||
|         get() = _searchUnifiedLiveData | ||||
|  | ||||
|     private var _searchCreatorLiveData = MutableLiveData<List<SearchResponseItem>>() | ||||
|     val searchCreatorLiveData: LiveData<List<SearchResponseItem>> | ||||
|         get() = _searchCreatorLiveData | ||||
|  | ||||
|     private var _searchContentLiveData = MutableLiveData<List<SearchResponseItem>>() | ||||
|     val searchContentLiveData: LiveData<List<SearchResponseItem>> | ||||
|         get() = _searchContentLiveData | ||||
|  | ||||
|     private var _searchSeriesLiveData = MutableLiveData<List<SearchResponseItem>>() | ||||
|     val searchSeriesLiveData: LiveData<List<SearchResponseItem>> | ||||
|         get() = _searchSeriesLiveData | ||||
|  | ||||
|     private var searchCreatorPage = 1 | ||||
|     private var searchContentPage = 1 | ||||
|     private var searchSeriesPage = 1 | ||||
|  | ||||
|     private var isSearchCreatorLast = false | ||||
|     private var isSearchContentLast = false | ||||
|     private var isSearchSeriesLast = false | ||||
|  | ||||
|     private val size = 20 | ||||
|  | ||||
|     fun changeTab(tab: SearchPageTab) { | ||||
|         _currentTabLiveData.value = tab | ||||
|     } | ||||
|  | ||||
|     fun searchUnified() { | ||||
|         if (!_isLoading.value!!) { | ||||
|             _currentTabLiveData.value = SearchPageTab.UNIFIED | ||||
|  | ||||
|             searchCreatorPage = 1 | ||||
|             searchContentPage = 1 | ||||
|             searchSeriesPage = 1 | ||||
|  | ||||
|             isSearchCreatorLast = false | ||||
|             isSearchContentLast = false | ||||
|             isSearchSeriesLast = false | ||||
|  | ||||
|             _isLoading.value = true | ||||
|             compositeDisposable.add( | ||||
|                 repository.searchUnified( | ||||
|                     keyword = keyword, | ||||
|                     token = "Bearer ${SharedPreferenceManager.token}" | ||||
|                 ) | ||||
|                     .subscribeOn(Schedulers.io()) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .subscribe( | ||||
|                         { | ||||
|                             _isLoading.value = false | ||||
|                             if (it.success && it.data != null) { | ||||
|                                 _searchUnifiedLiveData.value = it.data!! | ||||
|                             } else { | ||||
|                                 if (it.message != null) { | ||||
|                                     _toastLiveData.value = it.message | ||||
|                                 } else { | ||||
|                                     _toastLiveData | ||||
|                                         .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||
|                                 } | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             _isLoading.value = false | ||||
|                             it.message?.let { message -> Logger.e(message) } | ||||
|                             _toastLiveData | ||||
|                                 .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||
|                         } | ||||
|                     ) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun searchCreatorList() { | ||||
|         if (!_isLoading.value!! && !isSearchCreatorLast) { | ||||
|             _isLoading.value = true | ||||
|             compositeDisposable.add( | ||||
|                 repository.searchCreatorList( | ||||
|                     keyword = keyword, | ||||
|                     page = searchCreatorPage, | ||||
|                     size = size, | ||||
|                     token = "Bearer ${SharedPreferenceManager.token}" | ||||
|                 ) | ||||
|                     .subscribeOn(Schedulers.io()) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .subscribe( | ||||
|                         { | ||||
|                             _isLoading.value = false | ||||
|                             if (it.success && it.data != null) { | ||||
|                                 searchCreatorPage += 1 | ||||
|  | ||||
|                                 val data = it.data | ||||
|                                 _searchCreatorLiveData.value = data.items | ||||
|  | ||||
|                                 if (data.items.isEmpty()) { | ||||
|                                     isSearchCreatorLast = true | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 if (it.message != null) { | ||||
|                                     _toastLiveData.value = it.message | ||||
|                                 } else { | ||||
|                                     _toastLiveData | ||||
|                                         .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||
|                                 } | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             _isLoading.value = false | ||||
|                             it.message?.let { message -> Logger.e(message) } | ||||
|                             _toastLiveData | ||||
|                                 .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||
|                         } | ||||
|                     ) | ||||
|             ) | ||||
|         } else { | ||||
|             _searchCreatorLiveData.value = emptyList() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun searchContentList() { | ||||
|         if (!_isLoading.value!! && !isSearchContentLast) { | ||||
|             _isLoading.value = true | ||||
|             compositeDisposable.add( | ||||
|                 repository.searchContentList( | ||||
|                     keyword = keyword, | ||||
|                     page = searchContentPage, | ||||
|                     size = size, | ||||
|                     token = "Bearer ${SharedPreferenceManager.token}" | ||||
|                 ) | ||||
|                     .subscribeOn(Schedulers.io()) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .subscribe( | ||||
|                         { | ||||
|                             _isLoading.value = false | ||||
|                             if (it.success && it.data != null) { | ||||
|                                 searchContentPage += 1 | ||||
|  | ||||
|                                 val data = it.data | ||||
|                                 _searchContentLiveData.value = data.items | ||||
|  | ||||
|                                 if (data.items.isEmpty()) { | ||||
|                                     isSearchContentLast = true | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 if (it.message != null) { | ||||
|                                     _toastLiveData.value = it.message | ||||
|                                 } else { | ||||
|                                     _toastLiveData | ||||
|                                         .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||
|                                 } | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             _isLoading.value = false | ||||
|                             it.message?.let { message -> Logger.e(message) } | ||||
|                             _toastLiveData | ||||
|                                 .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||
|                         } | ||||
|                     ) | ||||
|             ) | ||||
|         } else { | ||||
|             _searchContentLiveData.value = emptyList() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun searchSeriesList() { | ||||
|         if (!_isLoading.value!! && !isSearchSeriesLast) { | ||||
|             _isLoading.value = true | ||||
|             compositeDisposable.add( | ||||
|                 repository.searchSeriesList( | ||||
|                     keyword = keyword, | ||||
|                     page = searchSeriesPage, | ||||
|                     size = size, | ||||
|                     token = "Bearer ${SharedPreferenceManager.token}" | ||||
|                 ) | ||||
|                     .subscribeOn(Schedulers.io()) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .subscribe( | ||||
|                         { | ||||
|                             _isLoading.value = false | ||||
|                             if (it.success && it.data != null) { | ||||
|                                 searchSeriesPage += 1 | ||||
|  | ||||
|                                 val data = it.data | ||||
|                                 _searchSeriesLiveData.value = data.items | ||||
|  | ||||
|                                 if (data.items.isEmpty()) { | ||||
|                                     isSearchSeriesLast = true | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 if (it.message != null) { | ||||
|                                     _toastLiveData.value = it.message | ||||
|                                 } else { | ||||
|                                     _toastLiveData | ||||
|                                         .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||
|                                 } | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             _isLoading.value = false | ||||
|                             it.message?.let { message -> Logger.e(message) } | ||||
|                             _toastLiveData | ||||
|                                 .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||
|                         } | ||||
|                     ) | ||||
|             ) | ||||
|         } else { | ||||
|             _searchSeriesLiveData.value = emptyList() | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										219
									
								
								app/src/main/res/layout/activity_search.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								app/src/main/res/layout/activity_search.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,219 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout 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:background="@color/black" | ||||
|     android:orientation="vertical"> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginEnd="13.3dp" | ||||
|         android:gravity="center_vertical" | ||||
|         android:paddingTop="13.3dp"> | ||||
|  | ||||
|         <ImageView | ||||
|             android:id="@+id/iv_back" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:contentDescription="@null" | ||||
|             android:padding="13.3dp" | ||||
|             android:src="@drawable/ic_back" /> | ||||
|  | ||||
|         <RelativeLayout | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="50dp" | ||||
|             android:background="@drawable/bg_round_corner_6_7_222222_bbbbbb"> | ||||
|  | ||||
|             <ImageView | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_centerVertical="true" | ||||
|                 android:layout_gravity="center_vertical" | ||||
|                 android:layout_marginStart="21.3dp" | ||||
|                 android:contentDescription="@null" | ||||
|                 android:src="@drawable/ic_title_search_black" /> | ||||
|  | ||||
|             <EditText | ||||
|                 android:id="@+id/et_search" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="match_parent" | ||||
|                 android:layout_gravity="center_vertical" | ||||
|                 android:background="@null" | ||||
|                 android:fontFamily="@font/gmarket_sans_medium" | ||||
|                 android:gravity="center_vertical" | ||||
|                 android:hint="검색" | ||||
|                 android:importantForAutofill="no" | ||||
|                 android:inputType="textWebEditText" | ||||
|                 android:paddingHorizontal="54.67dp" | ||||
|                 android:textColor="@color/color_eeeeee" | ||||
|                 android:textColorHint="@color/color_555555" | ||||
|                 android:textCursorDrawable="@drawable/edit_text_cursor" | ||||
|                 android:textSize="13.3sp" /> | ||||
|         </RelativeLayout> | ||||
|     </LinearLayout> | ||||
|  | ||||
|     <com.google.android.material.tabs.TabLayout | ||||
|         android:id="@+id/tabs" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginBottom="13.3dp" | ||||
|         android:background="@android:color/transparent" | ||||
|         android:clipToPadding="false" | ||||
|         android:elevation="0dp" | ||||
|         android:paddingHorizontal="13.3dp" | ||||
|         android:visibility="gone" | ||||
|         app:tabGravity="start" | ||||
|         app:tabIndicatorFullWidth="false" | ||||
|         app:tabIndicatorHeight="0dp" | ||||
|         app:tabMinWidth="45dp" | ||||
|         app:tabMode="scrollable" | ||||
|         app:tabPaddingBottom="15dp" | ||||
|         app:tabPaddingTop="15dp" | ||||
|         app:tabSelectedTextColor="@color/color_3bb9f1" | ||||
|         app:tabTextAppearance="@style/ContentMainTabText" | ||||
|         app:tabTextColor="@color/color_bbbbbb" /> | ||||
|  | ||||
|     <androidx.core.widget.NestedScrollView | ||||
|         android:id="@+id/ns_search_unified" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:visibility="gone"> | ||||
|  | ||||
|         <LinearLayout | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:orientation="vertical"> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/tv_creator_title" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_marginHorizontal="13.3dp" | ||||
|                 android:fontFamily="@font/gmarket_sans_bold" | ||||
|                 android:text="채널" | ||||
|                 android:textColor="@color/color_eeeeee" | ||||
|                 android:textSize="16sp" /> | ||||
|  | ||||
|             <androidx.recyclerview.widget.RecyclerView | ||||
|                 android:id="@+id/rv_unified_creator" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:clipToPadding="false" | ||||
|                 android:padding="13.3dp" /> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/tv_more_creator" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_marginHorizontal="13.3dp" | ||||
|                 android:layout_marginBottom="30dp" | ||||
|                 android:background="@color/color_cc333333" | ||||
|                 android:fontFamily="@font/gmarket_sans_medium" | ||||
|                 android:gravity="center" | ||||
|                 android:paddingVertical="10dp" | ||||
|                 android:text="더보기 >" | ||||
|                 android:textColor="@color/color_777777" | ||||
|                 android:textSize="13.3sp" /> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/tv_content_title" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_marginHorizontal="13.3dp" | ||||
|                 android:fontFamily="@font/gmarket_sans_bold" | ||||
|                 android:text="콘텐츠" | ||||
|                 android:textColor="@color/color_eeeeee" | ||||
|                 android:textSize="16sp" /> | ||||
|  | ||||
|             <androidx.recyclerview.widget.RecyclerView | ||||
|                 android:id="@+id/rv_unified_content" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:clipToPadding="false" | ||||
|                 android:padding="13.3dp" /> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/tv_more_content" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_marginHorizontal="13.3dp" | ||||
|                 android:layout_marginBottom="30dp" | ||||
|                 android:background="@color/color_cc333333" | ||||
|                 android:fontFamily="@font/gmarket_sans_medium" | ||||
|                 android:gravity="center" | ||||
|                 android:paddingVertical="10dp" | ||||
|                 android:text="더보기 >" | ||||
|                 android:textColor="@color/color_777777" | ||||
|                 android:textSize="13.3sp" /> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/tv_series_title" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_marginHorizontal="13.3dp" | ||||
|                 android:fontFamily="@font/gmarket_sans_bold" | ||||
|                 android:text="시리즈" | ||||
|                 android:textColor="@color/color_eeeeee" | ||||
|                 android:textSize="16sp" /> | ||||
|  | ||||
|             <androidx.recyclerview.widget.RecyclerView | ||||
|                 android:id="@+id/rv_unified_series" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:clipToPadding="false" | ||||
|                 android:padding="13.3dp" /> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/tv_more_series" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_marginHorizontal="13.3dp" | ||||
|                 android:layout_marginBottom="13.3dp" | ||||
|                 android:background="@color/color_cc333333" | ||||
|                 android:fontFamily="@font/gmarket_sans_medium" | ||||
|                 android:gravity="center" | ||||
|                 android:paddingVertical="10dp" | ||||
|                 android:text="더보기 >" | ||||
|                 android:textColor="@color/color_777777" | ||||
|                 android:textSize="13.3sp" /> | ||||
|         </LinearLayout> | ||||
|     </androidx.core.widget.NestedScrollView> | ||||
|  | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/rv_creator" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:clipToPadding="false" | ||||
|         android:padding="13.3dp" | ||||
|         android:visibility="gone" /> | ||||
|  | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/rv_content" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:clipToPadding="false" | ||||
|         android:padding="13.3dp" | ||||
|         android:visibility="gone" /> | ||||
|  | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/rv_series" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:clipToPadding="false" | ||||
|         android:padding="13.3dp" | ||||
|         android:visibility="gone" /> | ||||
|  | ||||
|     <TextView | ||||
|         android:id="@+id/tv_result_x" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginVertical="40dp" | ||||
|         android:fontFamily="@font/gmarket_sans_medium" | ||||
|         android:gravity="center" | ||||
|         android:text="검색 결과가 없습니다." | ||||
|         android:textColor="@color/white" | ||||
|         android:textSize="18.3sp" | ||||
|         android:visibility="gone" /> | ||||
| </LinearLayout> | ||||
							
								
								
									
										57
									
								
								app/src/main/res/layout/item_search.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								app/src/main/res/layout/item_search.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:background="@color/black" | ||||
|     android:gravity="center_vertical" | ||||
|     android:orientation="horizontal"> | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/iv_profile" | ||||
|         android:layout_width="60dp" | ||||
|         android:layout_height="60dp" | ||||
|         android:contentDescription="@null" | ||||
|         android:visibility="gone" /> | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/iv_content" | ||||
|         android:layout_width="60dp" | ||||
|         android:layout_height="60dp" | ||||
|         android:contentDescription="@null" | ||||
|         android:visibility="gone" /> | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/iv_series" | ||||
|         android:layout_width="60dp" | ||||
|         android:layout_height="85dp" | ||||
|         android:contentDescription="@null" | ||||
|         android:visibility="gone" /> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginStart="13.3dp" | ||||
|         android:orientation="vertical"> | ||||
|  | ||||
|         <TextView | ||||
|             android:id="@+id/tv_title" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:fontFamily="@font/gmarket_sans_medium" | ||||
|             android:textColor="@color/color_eeeeee" | ||||
|             android:textSize="13.3sp" | ||||
|             tools:text="slefjeiwok" /> | ||||
|  | ||||
|         <TextView | ||||
|             android:id="@+id/tv_nickname" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginTop="6.7dp" | ||||
|             android:fontFamily="@font/gmarket_sans_medium" | ||||
|             android:textColor="@color/color_777777" | ||||
|             android:textSize="10sp" | ||||
|             tools:ignore="SmallSp" | ||||
|             tools:text="slefjeiwok" /> | ||||
|     </LinearLayout> | ||||
| </LinearLayout> | ||||
		Reference in New Issue
	
	Block a user