feat(main-v2): 메인 하단 내비게이션을 추가한다

This commit is contained in:
2026-05-19 15:55:03 +09:00
parent 751b031627
commit 99b7a6ce99
41 changed files with 1646 additions and 22 deletions

View File

@@ -111,6 +111,7 @@
</intent-filter>
</activity>
<activity android:name=".main.MainActivity" />
<activity android:name=".v2.main.MainV2Activity" />
<activity android:name=".user.login.LoginActivity" />
<activity android:name=".audio_content.all.AudioContentAllActivity" />
<activity android:name=".settings.language.LanguageSettingsActivity" />

View File

@@ -22,7 +22,7 @@ import com.bumptech.glide.request.transition.Transition
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import org.koin.android.ext.android.inject
class AudioContentPlayService :
@@ -471,7 +471,7 @@ class AudioContentPlayService :
}
private fun updateNotification() {
val intent = Intent(this, MainActivity::class.java)
val intent = Intent(this, MainV2Activity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val pendingIntent = PendingIntent.getActivity(

View File

@@ -32,7 +32,7 @@ import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlayli
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.Utils
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import org.koin.android.ext.android.inject
@UnstableApi
@@ -153,7 +153,7 @@ class AudioContentPlayerService : MediaSessionService() {
}
private fun initMediaSession() {
val contextIntent = Intent(applicationContext, MainActivity::class.java).apply {
val contextIntent = Intent(applicationContext, MainV2Activity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val pendingIntent = PendingIntent.getActivity(

View File

@@ -176,6 +176,7 @@ import kr.co.vividnext.sodalive.user.UserViewModel
import kr.co.vividnext.sodalive.user.find_password.FindPasswordViewModel
import kr.co.vividnext.sodalive.user.login.LoginViewModel
import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
import kr.co.vividnext.sodalive.v2.main.MainV2ViewModel
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.android.ext.koin.androidContext
@@ -297,6 +298,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { TermsViewModel(get()) }
viewModel { FindPasswordViewModel(get()) }
viewModel { MainViewModel(get(), get(), get(), get(), get()) }
viewModel { MainV2ViewModel(get(), get(), get(), get(), get()) }
viewModel { LiveViewModel(get(), get(), get(), get(), get()) }
viewModel { MyPageViewModel(get(), get(), get()) }
viewModel { CanStatusViewModel(get()) }

View File

@@ -9,8 +9,8 @@ import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.databinding.ActivityLiveReservationCompleteBinding
import kr.co.vividnext.sodalive.extensions.convertDateFormat
import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.settings.language.LanguageManager
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import java.util.Locale
import java.util.TimeZone
@@ -52,7 +52,7 @@ class LiveReservationCompleteActivity : BaseActivity<ActivityLiveReservationComp
binding.tvRemainingCan.text = "${response.remainingCan}"
binding.tvGoHome.setOnClickListener {
val intent = Intent(applicationContext, MainActivity::class.java)
val intent = Intent(applicationContext, MainV2Activity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)

View File

@@ -16,6 +16,7 @@ import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
import kr.co.vividnext.sodalive.message.MessageActivity
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentActivity
import kr.co.vividnext.sodalive.splash.SplashActivity
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import java.util.Locale
class DeepLinkActivity : AppCompatActivity() {
@@ -63,7 +64,7 @@ class DeepLinkActivity : AppCompatActivity() {
}
startActivity(
Intent(applicationContext, MainActivity::class.java).apply {
Intent(applicationContext, MainV2Activity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
deepLinkExtras?.let { putExtra(Constants.EXTRA_DATA, it) }
}
@@ -465,7 +466,7 @@ class DeepLinkActivity : AppCompatActivity() {
}
startActivity(
Intent(applicationContext, MainActivity::class.java).apply {
Intent(applicationContext, MainV2Activity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
putExtra(Constants.EXTRA_DATA, extras)
}

View File

@@ -50,6 +50,7 @@ import kr.co.vividnext.sodalive.settings.notice.NoticeActivity
import kr.co.vividnext.sodalive.settings.notice.NoticeDetailActivity
import kr.co.vividnext.sodalive.settings.notification.MemberRole
import kr.co.vividnext.sodalive.splash.SplashActivity
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import org.koin.android.ext.android.inject
@UnstableApi
@@ -275,19 +276,19 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
binding.rlProfileContainer.visibility = View.GONE
binding.llProfileLoginContainer.visibility = View.VISIBLE
binding.llProfileLoginContainer.setOnClickListener {
(requireActivity() as MainActivity).showLoginActivity()
showLoginActivity()
}
binding.tvCanAmount.text =
SodaLiveApplicationHolder.get().getString(R.string.common_zero)
binding.tvCanAmount.setOnClickListener {
(requireActivity() as MainActivity).showLoginActivity()
showLoginActivity()
}
binding.tvPointAmount.text =
SodaLiveApplicationHolder.get().getString(R.string.common_zero)
binding.tvPointAmount.setOnClickListener {
(requireActivity() as MainActivity).showLoginActivity()
showLoginActivity()
}
binding.tvChargeCan.visibility = View.INVISIBLE
@@ -499,4 +500,10 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
}
}
private fun showLoginActivity() {
when (val activity = requireActivity()) {
is MainActivity -> activity.showLoginActivity()
is MainV2Activity -> activity.showLoginActivity()
}
}
}

View File

@@ -11,10 +11,10 @@ import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityCanStatusBinding
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
import kr.co.vividnext.sodalive.mypage.can.status.charge.CanChargeStatusFragment
import kr.co.vividnext.sodalive.mypage.can.status.use.CanUseStatusFragment
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import org.koin.android.ext.android.inject
class CanStatusActivity : BaseActivity<ActivityCanStatusBinding>(
@@ -137,7 +137,7 @@ class CanStatusActivity : BaseActivity<ActivityCanStatusBinding>(
}
private fun onClickBackButton() {
val intent = Intent(applicationContext, MainActivity::class.java)
val intent = Intent(applicationContext, MainV2Activity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)

View File

@@ -11,9 +11,9 @@ import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityPointStatusBinding
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.mypage.point.reward.PointRewardStatusFragment
import kr.co.vividnext.sodalive.mypage.point.use.PointUseStatusFragment
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import org.koin.android.ext.android.inject
class PointStatusActivity : BaseActivity<ActivityPointStatusBinding>(
@@ -120,7 +120,7 @@ class PointStatusActivity : BaseActivity<ActivityPointStatusBinding>(
}
private fun onClickBackButton() {
val intent = Intent(applicationContext, MainActivity::class.java)
val intent = Intent(applicationContext, MainV2Activity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)

View File

@@ -20,7 +20,7 @@ import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.databinding.ActivitySplashBinding
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
@SuppressLint("CustomSplashScreen")
class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding::inflate) {
@@ -174,7 +174,7 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding
private fun showMainActivity(extras: Bundle?) {
handler.postDelayed({
startActivity(
Intent(applicationContext, MainActivity::class.java).apply {
Intent(applicationContext, MainV2Activity::class.java).apply {
putExtra(Constants.EXTRA_DATA, extras)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)

View File

@@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.user.login
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
@@ -45,9 +44,9 @@ 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.ActivityLoginBinding
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.user.find_password.FindPasswordActivity
import kr.co.vividnext.sodalive.user.signup.SignUpActivity
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import org.koin.android.ext.android.inject
import java.util.UUID
import androidx.core.net.toUri
@@ -434,7 +433,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::i
}
private fun navigateToMain() {
val nextIntent = Intent(this@LoginActivity, MainActivity::class.java)
val nextIntent = Intent(this@LoginActivity, MainV2Activity::class.java)
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
?: if (intent.extras != null) {
intent.extras

View File

@@ -17,8 +17,8 @@ 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.ActivitySignupBinding
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.settings.terms.TermsActivity
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import org.koin.android.ext.android.inject
@OptIn(UnstableApi::class)
@@ -152,7 +152,7 @@ class SignUpActivity : BaseActivity<ActivitySignupBinding>(ActivitySignupBinding
}
private fun navigateToMain() {
val nextIntent = Intent(this@SignUpActivity, MainActivity::class.java)
val nextIntent = Intent(this@SignUpActivity, MainV2Activity::class.java)
val extras = intent.getBundleExtra(Constants.EXTRA_DATA)
?: if (intent.extras != null) {
intent.extras

View File

@@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.v2.main
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.databinding.FragmentV2MainChatBinding
class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
FragmentV2MainChatBinding::inflate
)

View File

@@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.v2.main
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
FragmentV2MainContentBinding::inflate
)

View File

@@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.v2.main
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.databinding.FragmentV2MainHomeBinding
class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
FragmentV2MainHomeBinding::inflate
)

View File

@@ -0,0 +1,654 @@
package kr.co.vividnext.sodalive.v2.main
import android.Manifest
import android.content.ComponentName
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import coil.load
import coil.transform.RoundedCornersTransformation
import com.google.common.util.concurrent.ListenableFuture
import com.google.firebase.messaging.FirebaseMessaging
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.orhanobut.logger.Logger
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerFragment
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.audition.AuditionActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityMainV2Binding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
import kr.co.vividnext.sodalive.main.EventPopupDialogFragment
import kr.co.vividnext.sodalive.message.MessageActivity
import kr.co.vividnext.sodalive.mypage.MyPageFragment
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsDialog
import kr.co.vividnext.sodalive.user.login.LoginActivity
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import java.util.Locale
import kotlin.math.max
@UnstableApi
class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding::inflate) {
private val viewModel: MainV2ViewModel by inject()
private lateinit var notificationSettingsDialog: NotificationSettingsDialog
private var mediaController: MediaController? = null
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
private val handler = Handler(Looper.getMainLooper())
private val showMiniPlayerRunnable = Runnable { initAndVisibleMiniPlayer() }
private var playerStateJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
overrideRootWindowInsets()
checkPermissions()
trackAppLaunchIfNeeded()
pushTokenUpdate()
if (isLoggedIn()) {
updatePidAndGaid()
getEventPopup()
observePlayerState()
handler.postDelayed({ executeDeeplink(intent) }, 1000)
}
}
private fun overrideRootWindowInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
val left = max(systemBars.left, ime.left)
val top = systemBars.top
val right = max(systemBars.right, ime.right)
v.setPadding(left, top, right, 0)
insets
}
ViewCompat.requestApplyInsets(binding.root)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
if (isLoggedIn()) {
executeDeeplink(intent)
}
}
override fun onResume() {
super.onResume()
getMemberInfo()
startService(
Intent(this, AudioContentPlayService::class.java).apply {
action = AudioContentPlayService.MusicAction.INIT.name
}
)
}
override fun onDestroy() {
deInitMiniPlayer()
playerStateJob?.cancel()
super.onDestroy()
}
override fun setupView() {
notificationSettingsDialog = NotificationSettingsDialog(
this,
layoutInflater
) { isNotifiedLive, isNotifiedUploadContent, isNotifiedMessage ->
viewModel.updateNotificationSettings(
isNotifiedLive,
isNotifiedUploadContent,
isNotifiedMessage
)
}
setupBottomNavigation()
}
fun showLoginActivity() {
if (SharedPreferenceManager.token.isBlank()) {
val extras = intent.extras
startActivity(
Intent(applicationContext, LoginActivity::class.java).apply {
putExtra(Constants.EXTRA_DATA, extras)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
)
}
}
fun openChatTab() {
viewModel.clickTab(MainV2Tab.CHAT)
}
private fun setupBottomNavigation() {
binding.bottomNavigation.setOnItemSelectedListener { item ->
when (item.itemId) {
R.id.menu_main_v2_home -> viewModel.clickTab(MainV2Tab.HOME)
R.id.menu_main_v2_content -> viewModel.clickTab(MainV2Tab.CONTENT)
R.id.menu_main_v2_chat -> viewModel.clickTab(MainV2Tab.CHAT)
R.id.menu_main_v2_my -> viewModel.clickTab(MainV2Tab.MY)
}
true
}
binding.bottomNavigation.apply {
itemIconTintList = null
}
viewModel.currentTab.observe(this) { tab ->
val itemId = when (tab) {
MainV2Tab.HOME -> R.id.menu_main_v2_home
MainV2Tab.CONTENT -> R.id.menu_main_v2_content
MainV2Tab.CHAT -> R.id.menu_main_v2_chat
MainV2Tab.MY -> R.id.menu_main_v2_my
}
if (binding.bottomNavigation.selectedItemId != itemId) {
binding.bottomNavigation.selectedItemId = itemId
}
changeFragment(tab)
}
}
private fun changeFragment(currentTab: MainV2Tab) {
val tag = currentTab.toString()
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
fragmentManager.primaryNavigationFragment?.let {
fragmentTransaction.hide(it)
}
var fragment = fragmentManager.findFragmentByTag(tag)
if (fragment == null) {
fragment = when (currentTab) {
MainV2Tab.HOME -> HomeMainFragment()
MainV2Tab.CONTENT -> ContentMainFragment()
MainV2Tab.CHAT -> ChatMainFragment()
MainV2Tab.MY -> MyPageFragment()
}
fragmentTransaction.add(R.id.fl_container, fragment, tag)
} else {
fragmentTransaction.show(fragment)
}
fragmentTransaction.setPrimaryNavigationFragment(fragment)
fragmentTransaction.setReorderingAllowed(true)
fragmentTransaction.commitNow()
}
private fun observePlayerState() {
playerStateJob = lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
SharedPreferenceManager.isPlayerServiceRunningFlow.collect { isRunning ->
if (isRunning) {
handler.removeCallbacks(showMiniPlayerRunnable)
handler.postDelayed(showMiniPlayerRunnable, 1500)
} else {
deInitMiniPlayer()
}
}
}
}
}
private fun initAndVisibleMiniPlayer() {
binding.clMiniPlayer.visibility = View.VISIBLE
binding.clMiniPlayer.setOnClickListener { showPlayerFragment() }
binding.ivPlayerStop.setOnClickListener {
startService(
Intent(applicationContext, AudioContentPlayerService::class.java).apply {
action = "STOP_SERVICE"
}
)
}
connectPlayerService()
}
private fun connectPlayerService() {
if (mediaController != null || mediaControllerFuture != null) {
return
}
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
val sessionToken = SessionToken(applicationContext, componentName)
val controllerFuture =
MediaController.Builder(applicationContext, sessionToken).buildAsync()
mediaControllerFuture = controllerFuture
controllerFuture.addListener(
{
try {
if (mediaController != null) {
controllerFuture.get().release()
return@addListener
}
mediaController = controllerFuture.get()
setupMediaController()
updateMediaMetadata(mediaController?.mediaMetadata)
binding.ivPlayerPlayOrPause.setImageResource(
if (mediaController?.isPlaying == true) {
R.drawable.ic_player_pause
} else {
R.drawable.ic_player_play
}
)
binding.ivPlayerPlayOrPause.setOnClickListener {
mediaController?.let {
if (it.playWhenReady) {
it.pause()
} else {
it.play()
}
}
}
} catch (throwable: Throwable) {
Logger.e(throwable, "Failed to connect player service")
} finally {
mediaControllerFuture = null
}
},
ContextCompat.getMainExecutor(applicationContext)
)
}
private fun updateMediaMetadata(metadata: MediaMetadata?) {
metadata?.let {
binding.tvPlayerTitle.text = it.title
binding.tvPlayerNickname.text = it.artist
binding.ivPlayerCover.load(it.artworkUri) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4f))
}
}
}
private fun setupMediaController() {
if (mediaController == null) {
deInitMiniPlayer()
return
}
mediaController!!.addListener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
updateMediaMetadata(mediaItem?.mediaMetadata)
}
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
binding.ivPlayerPlayOrPause.setImageResource(
if (playWhenReady) {
R.drawable.ic_player_pause
} else {
R.drawable.ic_player_play
}
)
}
})
}
private fun deInitMiniPlayer() {
handler.removeCallbacks(showMiniPlayerRunnable)
binding.clMiniPlayer.visibility = View.GONE
mediaControllerFuture?.cancel(true)
mediaControllerFuture = null
mediaController?.release()
mediaController = null
}
private fun showPlayerFragment() {
val playerFragment = AudioContentPlayerFragment(screenWidth, arrayListOf())
playerFragment.show(supportFragmentManager, playerFragment.tag)
}
private fun checkPermissions() {
val permissions = mutableListOf(Manifest.permission.RECORD_AUDIO)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissions.add(Manifest.permission.POST_NOTIFICATIONS)
}
TedPermission.create()
.setPermissionListener(object : PermissionListener {
override fun onPermissionGranted() {
}
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
}
})
.setDeniedMessage(R.string.record_audio_permission_denied_message)
.setPermissions(*permissions.toTypedArray())
.check()
}
private fun trackAppLaunchIfNeeded() {
handler.postDelayed({
val alreadyTrackingAppLaunch = SharedPreferenceManager.alreadyTrackingAppLaunch
val pid = SharedPreferenceManager.marketingPid
if (!alreadyTrackingAppLaunch && pid.isNotBlank()) {
SharedPreferenceManager.alreadyTrackingAppLaunch = true
viewModel.adTrackingAppLaunch(pid = pid)
}
}, 1000)
}
private fun pushTokenUpdate() {
FirebaseMessaging.getInstance().token.addOnCompleteListener {
if (!it.isSuccessful) {
Logger.v("Fetching FCM registration token failed", it.exception)
return@addOnCompleteListener
}
val pushToken = it.result
if (pushToken != null) {
SharedPreferenceManager.pushToken = pushToken
if (isLoggedIn()) {
viewModel.pushTokenUpdate(pushToken)
}
}
}
}
private fun updatePidAndGaid() {
handler.postDelayed({
viewModel.fetchAndUpdateGaidAndPid(context = applicationContext)
}, 3000)
}
private fun getMemberInfo() {
if (isLoggedIn()) {
viewModel.getMemberInfo(context = applicationContext) {
notificationSettingsDialog.show(screenWidth)
}
}
}
private fun getEventPopup() {
viewModel.getEventPopup {
if (SharedPreferenceManager.notShowingEventPopupId != it.id) {
EventPopupDialogFragment(
screenWidth = screenWidth,
eventItem = it
) {
startActivity(
Intent(applicationContext, EventDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_EVENT, it)
}
)
}.show(supportFragmentManager, EventPopupDialogFragment::class.java.simpleName)
}
}
}
private fun executeDeeplink(intent: Intent) {
val bundle = intent.getBundleExtra(Constants.EXTRA_DATA) ?: return
val deepLinkUrl = bundle.getString("deep_link")
val routeBundle = if (!deepLinkUrl.isNullOrBlank()) {
buildBundleFromDeepLinkUrl(deepLinkUrl) ?: bundle
} else {
bundle
}
if (executeBundleRoute(routeBundle)) {
clearDeferredDeepLink()
}
}
private fun buildBundleFromDeepLinkUrl(deepLinkUrl: String): Bundle? {
val data = runCatching { deepLinkUrl.toUri() }.getOrNull() ?: return null
val extras = Bundle().apply {
putString("deep_link", deepLinkUrl)
}
fun putQuery(key: String) {
val value = data.getQueryParameter(key)
if (!value.isNullOrBlank()) {
extras.putString(key, value)
}
}
putQuery("channel_id")
putQuery("message_id")
putQuery("audition_id")
putQuery("content_id")
putQuery("deep_link_value")
putQuery("deep_link_sub5")
putQuery(Constants.EXTRA_COMMUNITY_CREATOR_ID)
putQuery(Constants.EXTRA_COMMUNITY_POST_ID)
applyPathDeepLink(data = data) { key, value ->
if (!value.isNullOrBlank() && !extras.containsKey(key)) {
extras.putString(key, value)
}
}
return extras
}
private fun applyPathDeepLink(
data: android.net.Uri,
putIfAbsent: (key: String, value: String?) -> Unit
) {
val host = data.host?.lowercase(Locale.ROOT).orEmpty()
val pathSegments = data.pathSegments.filter { it.isNotBlank() }
val pathType: String
val pathId: String?
if (host.isNotBlank() && host != "payverse") {
pathType = host
pathId = pathSegments.firstOrNull()
} else if (pathSegments.isNotEmpty()) {
pathType = pathSegments[0].lowercase(Locale.ROOT)
pathId = pathSegments.getOrNull(1)
} else {
return
}
when (pathType) {
"content" -> {
putIfAbsent("content_id", pathId)
putIfAbsent("deep_link_value", "content")
putIfAbsent("deep_link_sub5", pathId)
}
"series" -> {
putIfAbsent("deep_link_value", "series")
putIfAbsent("deep_link_sub5", pathId)
}
"community" -> {
putIfAbsent("deep_link_value", "community")
putIfAbsent(Constants.EXTRA_COMMUNITY_CREATOR_ID, pathId)
putIfAbsent("deep_link_sub5", pathId)
}
"message" -> {
putIfAbsent("deep_link_value", "message")
putIfAbsent("message_id", pathId)
putIfAbsent("deep_link_sub5", pathId)
}
"audition" -> {
putIfAbsent("deep_link_value", "audition")
putIfAbsent("audition_id", pathId)
putIfAbsent("deep_link_sub5", pathId)
}
}
}
private fun executeBundleRoute(bundle: Bundle): Boolean {
val channelId = bundle.getString("channel_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_USER_ID).takeIf { it > 0 }
val messageId = bundle.getString("message_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_MESSAGE_ID).takeIf { it > 0 }
val contentId = bundle.getString("content_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_AUDIO_CONTENT_ID).takeIf { it > 0 }
val auditionId = bundle.getString("audition_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_AUDITION_ID).takeIf { it > 0 }
val communityCreatorId = bundle.getString(Constants.EXTRA_COMMUNITY_CREATOR_ID)?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_COMMUNITY_CREATOR_ID).takeIf { it > 0 }
val communityPostId = bundle.getString(Constants.EXTRA_COMMUNITY_POST_ID)?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_COMMUNITY_POST_ID).takeIf { it > 0 }
when {
channelId != null && channelId > 0 -> {
startActivity(
Intent(applicationContext, UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, channelId)
}
)
return true
}
contentId != null && contentId > 0 -> {
startActivity(
Intent(applicationContext, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
)
return true
}
messageId != null && messageId > 0 -> {
startActivity(Intent(applicationContext, MessageActivity::class.java))
return true
}
communityCreatorId != null && communityCreatorId > 0 -> {
startActivity(
Intent(applicationContext, CreatorCommunityAllActivity::class.java).apply {
putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, communityCreatorId)
if (communityPostId != null && communityPostId > 0) {
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, communityPostId)
}
}
)
return true
}
auditionId != null && auditionId > 0 -> {
startActivity(Intent(applicationContext, AuditionActivity::class.java))
return true
}
}
val deepLinkValue = bundle.getString("deep_link_value")
val deepLinkValueId = bundle.getString("deep_link_sub5")?.toLongOrNull()
return !deepLinkValue.isNullOrBlank() && routeByDeepLinkValue(deepLinkValue, deepLinkValueId)
}
private fun routeByDeepLinkValue(deepLinkValue: String, deepLinkValueId: Long?): Boolean {
return when (deepLinkValue.lowercase(Locale.ROOT)) {
"series" -> {
if (deepLinkValueId == null || deepLinkValueId <= 0) {
return false
}
startActivity(
Intent(applicationContext, SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, deepLinkValueId)
}
)
true
}
"content" -> {
if (deepLinkValueId == null || deepLinkValueId <= 0) {
return false
}
startActivity(
Intent(applicationContext, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, deepLinkValueId)
}
)
true
}
"channel" -> {
if (deepLinkValueId == null || deepLinkValueId <= 0) {
return false
}
startActivity(
Intent(applicationContext, UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, deepLinkValueId)
}
)
true
}
"community" -> {
if (deepLinkValueId == null || deepLinkValueId <= 0) {
return false
}
startActivity(
Intent(applicationContext, CreatorCommunityAllActivity::class.java).apply {
putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, deepLinkValueId)
}
)
true
}
"message" -> {
startActivity(Intent(applicationContext, MessageActivity::class.java))
true
}
"audition" -> {
startActivity(Intent(applicationContext, AuditionActivity::class.java))
true
}
else -> false
}
}
private fun clearDeferredDeepLink() {
SharedPreferenceManager.marketingUtmSource = ""
SharedPreferenceManager.marketingUtmMedium = ""
SharedPreferenceManager.marketingUtmCampaign = ""
SharedPreferenceManager.marketingLinkValue = ""
SharedPreferenceManager.marketingLinkValueId = 0
}
private fun isLoggedIn(): Boolean {
return SharedPreferenceManager.token.isNotBlank() && SharedPreferenceManager.token.length > 10
}
}

View File

@@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.v2.main
enum class MainV2Tab {
HOME,
CONTENT,
CHAT,
MY
}

View File

@@ -0,0 +1,231 @@
package kr.co.vividnext.sodalive.v2.main
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.android.gms.ads.identifier.AdvertisingIdClient
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AddAllPlaybackTrackingRequest
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.PlaybackTrackingData
import kr.co.vividnext.sodalive.audio_content.PlaybackTrackingRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.main.MarketingInfoUpdateRequest
import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest
import kr.co.vividnext.sodalive.settings.ContentType
import kr.co.vividnext.sodalive.settings.event.EventItem
import kr.co.vividnext.sodalive.settings.event.EventRepository
import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest
import kr.co.vividnext.sodalive.tracking.AdTrackingRepository
import kr.co.vividnext.sodalive.tracking.FirebaseTracking
import kr.co.vividnext.sodalive.tracking.NotiflyClient
import kr.co.vividnext.sodalive.user.UserRepository
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.Executors
class MainV2ViewModel(
private val userRepository: UserRepository,
private val eventRepository: EventRepository,
private val adTrackingRepository: AdTrackingRepository,
private val audioContentRepository: AudioContentRepository,
private val playbackTrackingRepository: PlaybackTrackingRepository
) : BaseViewModel() {
private val _currentTab = MutableLiveData(MainV2Tab.HOME)
val currentTab: LiveData<MainV2Tab>
get() = _currentTab
fun clickTab(tab: MainV2Tab) {
if (_currentTab.value != tab) {
_currentTab.postValue(tab)
}
}
fun updateNotificationSettings(
isNotifiedLive: Boolean,
isNotifiedUploadContent: Boolean,
isNotifiedMessage: Boolean
) {
compositeDisposable.add(
userRepository.updateNotificationSettings(
request = UpdateNotificationSettingRequest(
isNotifiedLive,
isNotifiedUploadContent,
isNotifiedMessage
),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({}, {})
)
}
fun pushTokenUpdate(pushToken: String) {
compositeDisposable.add(
userRepository
.updatePushToken(
PushTokenUpdateRequest(pushToken = pushToken),
"Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({}, {})
)
}
fun getMemberInfo(context: Context, showNotificationSettingsDialog: () -> Unit) {
compositeDisposable.add(
userRepository.getMemberInfo(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val data = it.data
SharedPreferenceManager.can = data.can
SharedPreferenceManager.point = data.point
SharedPreferenceManager.role = data.role.name
SharedPreferenceManager.isAuth = data.isAuth
val localCountryCode = SharedPreferenceManager.countryCode.ifBlank { "KR" }
val resolvedCountryCode = data.countryCode?.ifBlank { "KR" } ?: localCountryCode
val resolvedIsAdultContentVisible =
data.isAdultContentVisible ?: SharedPreferenceManager.isAdultContentVisible
val resolvedContentType =
data.contentType
?: ContentType.entries.getOrNull(SharedPreferenceManager.contentPreference)
?: ContentType.ALL
SharedPreferenceManager.countryCode = resolvedCountryCode
SharedPreferenceManager.isAdultContentVisible = resolvedIsAdultContentVisible
SharedPreferenceManager.contentPreference = resolvedContentType.ordinal
SharedPreferenceManager.isAuditionNotification =
data.auditionNotice ?: false
if (
data.followingChannelUploadContentNotice == null &&
data.followingChannelLiveNotice == null &&
data.messageNotice == null
) {
showNotificationSettingsDialog()
}
val dateFormat = SimpleDateFormat(
"yyyy-MM-dd, HH:mm:ss",
Locale.getDefault()
)
val lastActiveDate = dateFormat.format(Date())
val params = mutableMapOf(
"nickname" to SharedPreferenceManager.nickname,
"last_active_date" to lastActiveDate,
"charge_count" to data.chargeCount,
"signup_date" to data.signupDate,
"is_auth" to data.isAuth,
"gender" to data.gender,
"can" to data.can
)
NotiflyClient.setUser(
context = context,
userId = SharedPreferenceManager.userId,
params = params
)
FirebaseTracking.login("email")
}
},
{}
)
)
}
fun addAllPlaybackTracking() {
val trackingDataList = playbackTrackingRepository.getAllPlaybackTracking()
.filter { it.endPosition != null }
.filter { it.endPosition!! - it.startPosition >= 4000 }
.map {
PlaybackTrackingData(it.contentId, it.playDateTime, it.isPreview)
}
if (trackingDataList.isNotEmpty()) {
compositeDisposable.add(
audioContentRepository.addAllPlaybackTracking(
request = AddAllPlaybackTrackingRequest(trackingDataList = trackingDataList),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
playbackTrackingRepository.removeAllPlaybackTracking()
}
},
{}
)
)
}
}
fun getEventPopup(onSuccess: (EventItem) -> Unit) {
compositeDisposable.add(
eventRepository.getEventPopup(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
onSuccess(it.data)
}
},
{}
)
)
}
fun fetchAndUpdateGaidAndPid(context: Context) {
Executors.newSingleThreadExecutor().execute {
try {
val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context)
val request = MarketingInfoUpdateRequest(
adid = adInfo.id.orEmpty(),
pid = SharedPreferenceManager.marketingPid
)
updateMarketingInfo(request)
} catch (e: Exception) {
e.printStackTrace()
updateMarketingInfo(
MarketingInfoUpdateRequest(
adid = "",
pid = SharedPreferenceManager.marketingPid
)
)
}
}
}
fun adTrackingAppLaunch(pid: String) {
compositeDisposable.add(
adTrackingRepository.appLaunch(pid)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({}, {})
)
}
private fun updateMarketingInfo(request: MarketingInfoUpdateRequest) {
compositeDisposable.add(
userRepository.updateMarketingInfo(
request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({}, {})
)
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/white" android:state_checked="true" />
<item android:color="@color/white" android:state_selected="true" />
<item android:color="@color/gray_600" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_nav_chat_selected" android:state_checked="true" />
<item android:drawable="@drawable/ic_nav_chat" />
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_nav_content_selected" android:state_checked="true" />
<item android:drawable="@drawable/ic_nav_content" />
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_nav_home_selected" android:state_checked="true" />
<item android:drawable="@drawable/ic_nav_home" />
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_tabbar_my_selected" android:state_checked="true" />
<item android:drawable="@drawable/ic_nav_my" />
</selector>

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<FrameLayout
android:id="@+id/fl_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/cl_mini_player"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_mini_player"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/color_222222"
android:paddingHorizontal="13.3dp"
android:paddingVertical="10.7dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible">
<ImageView
android:id="@+id/iv_player_cover"
android:layout_width="36.7dp"
android:layout_height="36.7dp"
android:contentDescription="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/tv_player_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10.7dp"
android:ellipsize="end"
android:fontFamily="@font/medium"
android:maxLines="2"
android:textColor="@color/color_eeeeee"
android:textSize="13sp"
app:layout_constraintEnd_toStartOf="@+id/iv_player_play_or_pause"
app:layout_constraintStart_toEndOf="@+id/iv_player_cover"
app:layout_constraintTop_toTopOf="@+id/iv_player_cover"
tools:text="JFLA 커버곡 Avicii for your self" />
<TextView
android:id="@+id/tv_player_nickname"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2.3dp"
android:fontFamily="@font/medium"
android:textColor="@color/color_d2d2d2"
android:textSize="11sp"
app:layout_constraintEnd_toEndOf="@+id/tv_player_title"
app:layout_constraintStart_toStartOf="@+id/tv_player_title"
app:layout_constraintTop_toBottomOf="@+id/tv_player_title"
tools:ignore="SmallSp"
tools:text="JFLA 커버곡 Avicii for your self" />
<ImageView
android:id="@+id/iv_player_play_or_pause"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginEnd="16dp"
android:contentDescription="@null"
app:layout_constraintBottom_toBottomOf="@+id/iv_player_stop"
app:layout_constraintEnd_toStartOf="@+id/iv_player_stop"
app:layout_constraintTop_toTopOf="@+id/iv_player_stop"
tools:src="@drawable/btn_bar_play" />
<ImageView
android:id="@+id/iv_player_stop"
android:layout_width="25dp"
android:layout_height="25dp"
android:contentDescription="@null"
android:src="@drawable/ic_noti_stop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/black"
app:itemActiveIndicatorStyle="@null"
app:itemIconTint="@null"
app:itemTextAppearanceActive="@style/Typography.Caption3"
app:itemTextAppearanceActiveBoldEnabled="false"
app:itemTextAppearanceInactive="@style/Typography.Caption3"
app:itemTextColor="@color/color_main_v2_bottom_navigation_label"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/menu_main_v2_bottom_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/black"
android:orientation="vertical">
<ImageView

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black" />

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black" />

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black" />

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_main_v2_home"
android:icon="@drawable/ic_nav_home_tab"
android:title="@string/tab_home" />
<item
android:id="@+id/menu_main_v2_content"
android:icon="@drawable/ic_nav_content_tab"
android:title="@string/tab_content" />
<item
android:id="@+id/menu_main_v2_chat"
android:icon="@drawable/ic_nav_chat_tab"
android:title="@string/tab_chat" />
<item
android:id="@+id/menu_main_v2_my"
android:icon="@drawable/ic_nav_my_tab"
android:title="@string/tab_my" />
</menu>

View File

@@ -137,6 +137,7 @@
<!-- Main / Home -->
<string name="tab_home">Home</string>
<string name="tab_content">Content</string>
<string name="tab_chat">Chat</string>
<string name="tab_live">Live</string>
<string name="tab_my">My</string>

View File

@@ -137,6 +137,7 @@
<!-- Main / Home -->
<string name="tab_home">ホーム</string>
<string name="tab_content">コンテンツ</string>
<string name="tab_chat">チャット</string>
<string name="tab_live">ライブ</string>
<string name="tab_my">マイ</string>

View File

@@ -136,6 +136,7 @@
<!-- Main / Home -->
<string name="tab_home"></string>
<string name="tab_content">콘텐츠</string>
<string name="tab_chat">채팅</string>
<string name="tab_live">라이브</string>
<string name="tab_my">마이</string>