Compare commits

...

46 Commits

Author SHA1 Message Date
klaus ad7a5b36f1 푸시, 딥링크 - 라이브 탭으로 이동하지 않아도 실행되도록 수정 2023-08-31 20:04:33 +09:00
klaus 9eb136379c 코인 -> 캔 2023-08-29 23:39:10 +09:00
klaus 735f353f31 콘텐츠 대여가격 60%로 변경 2023-08-29 23:29:33 +09:00
klaus a422eb08c0 응원 전체보기 추가 2023-08-29 17:31:04 +09:00
klaus d1b97aafd2 후원랭킹 전체보기 추가 2023-08-29 11:24:06 +09:00
klaus 110fcab710 콘텐츠 등록 - 5캔(500원) 부터 등록되도록 수정, 대여가격 안내 60%로 수정 2023-08-28 18:19:04 +09:00
klaus 91af371523 라이브 방 - 공지사항 수정 가능 2023-08-28 18:10:04 +09:00
klaus 2ee4c3e9ad 푸시 메시지 - 채널 id 추가 2023-08-28 17:56:28 +09:00
klaus 96c72c4fa0 라이브 후원 다이얼로그 - 키보드가 올라왔을 때 화면이 전체적으로 위로 이동하여 보이지 않는 부분이 없어지도록 수정 2023-08-25 21:56:03 +09:00
klaus 0a56ef1227 라이브 방 - 스피커 최대 10 -> 5명으로 수정 2023-08-21 05:37:49 +09:00
klaus 6a06a46713 메시지 탭 보이도록 수정 2023-08-21 04:41:44 +09:00
klaus 69f2cbd4a2 본인인증을 한 유저만 메시지 탭이 보이도록 수정 2023-08-21 04:20:09 +09:00
klaus f0e841ffbe 라이브 상세 - 제목 왼쪽에 19금 표시 추가 2023-08-21 04:08:37 +09:00
klaus b8d11d2276 로딩뷰 변경 2023-08-21 04:00:11 +09:00
klaus 28d5f3a6f6 라이브 메인 - 이벤트 배너 터치이벤트 추가 2023-08-21 01:35:05 +09:00
klaus 8e135e893e 라이브 방 - 입장메시지 방장만 보이도록 수정 2023-08-21 01:32:01 +09:00
klaus 01512abaf0 라이브 방 - 19금 방인 경우 제목 왼쪽에 19 표시 2023-08-20 23:44:41 +09:00
klaus bb7476f640 마이페이지 탭 - 구매내역 추가 2023-08-20 23:37:17 +09:00
klaus 875d8361f3 이용약관/개인정보처리방침 링크 수정 2023-08-20 20:57:52 +09:00
klaus 36ffa4fa58 메인 하단 탭 - 콘텐츠, 라이브 순서 변경 2023-08-20 20:46:29 +09:00
klaus 3b868294b0 19금 표시 제거 2023-08-20 17:46:16 +09:00
klaus 508eff2cd0 크리에이터 채널 - 콘텐츠 영역 추가 2023-08-20 03:46:13 +09:00
klaus 9bc625b7a0 socdoc -> message 로 변경 2023-08-19 22:54:38 +09:00
klaus 98d895f91f 사용방법 블로그 URL 수정 2023-08-19 22:53:10 +09:00
klaus bf75835c69 ProfileResponse - Coin 을 Can으로 변경 2023-08-19 16:25:15 +09:00
klaus 4ac272cd9d 콘텐츠 메인 - SerializeName 변경 audioContents -> contents 2023-08-18 23:14:56 +09:00
klaus a5abb39059 이미지 로딩 라이브러리 변경 2023-08-18 21:51:38 +09:00
klaus f7299cc0df mediaPlayer 를 사용하는 곳은 항상 초기화 되어 있는지 확인 하고 로직이 실행되도록 수정 2023-08-18 21:06:53 +09:00
klaus a4cdbebeb4 본인이 만든 라이브의 경우 팔로잉 버튼, 선물하기 버튼이 보이지 않도록 수정 2023-08-18 20:56:26 +09:00
klaus 2e48dad913 라이브 방 상세 - 예약자/참여자 리스트 제거, 채널보기 버튼 액션 추가 2023-08-18 19:51:35 +09:00
klaus f6d6f1de8d 라이브 방 - 키보드가 올라오면 화면이 전체적으로 위로 올라가도록 수정 2023-08-18 19:41:21 +09:00
klaus be7c7d0682 프로필 수정 페이지 추가 2023-08-18 19:39:04 +09:00
klaus 9adadaf572 공유링크 수정 2023-08-18 16:54:55 +09:00
klaus ca30e6949f 앱 런처 아이콘 변경 2023-08-18 16:43:35 +09:00
klaus 9fa6ccb786 온보딩 튜토리얼 추가 2023-08-18 16:41:36 +09:00
klaus 3d96fab0f6 placeholder 이미지 변경 2023-08-17 23:59:14 +09:00
klaus 0524615ee4 소다라이브 사용방법 배너 마이페이지로 이동 2023-08-17 23:54:47 +09:00
klaus e165813545 캔 아이콘 변경 2023-08-17 23:41:16 +09:00
klaus 7aaac27d24 고객센터 카톡 URL 변경 2023-08-17 14:03:56 +09:00
klaus 66006853bd AGP 7.3.1 로 변경 2023-08-17 13:53:48 +09:00
klaus 79a241f436 proguard rule 추가 2023-08-16 19:09:52 +09:00
klaus 0e24eab7b3 firebase version 32.2.2 로 변경 2023-08-16 18:21:03 +09:00
klaus ea79ffcc02 add real service firebase config file 2023-08-16 18:11:47 +09:00
klaus 90655c692d manager -> creator 2023-08-15 00:11:52 +09:00
klaus 7e9bcb6c38 사용하지 않는 필드 제거 2023-08-14 22:56:57 +09:00
klaus 999f90ae80 coin -> can,
코인 -> 캔

으로 변경
2023-08-14 11:15:21 +09:00
189 changed files with 4425 additions and 1171 deletions

View File

@ -9,7 +9,7 @@ plugins {
id 'org.jlleitschuh.gradle.ktlint'
id 'io.objectbox'
id("com.google.firebase.crashlytics")
id 'com.google.firebase.crashlytics'
}
android {
@ -39,8 +39,8 @@ android {
applicationId "kr.co.vividnext.sodalive"
minSdk 23
targetSdk 33
versionCode 1
versionName "1.0.0"
versionCode 2
versionName "1.0.1"
}
buildTypes {
@ -98,13 +98,13 @@ dependencies {
implementation "io.insert-koin:koin-android:3.1.3"
// Preference
implementation("androidx.preference:preference-ktx:1.2.0") {
implementation("androidx.preference:preference-ktx:1.2.1") {
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
}
// Gson
implementation "com.google.code.gson:gson:2.9.1"
implementation "com.google.code.gson:gson:2.10.1"
// Network
implementation "com.squareup.retrofit2:retrofit:2.9.0"
@ -113,8 +113,8 @@ dependencies {
implementation "com.squareup.okhttp3:logging-interceptor:4.9.3"
// RxJava3
implementation "io.reactivex.rxjava3:rxjava:3.1.3"
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
implementation "io.reactivex.rxjava3:rxjava:3.1.6"
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// permission
@ -126,7 +126,7 @@ dependencies {
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.1'
// Firebase
implementation platform('com.google.firebase:firebase-bom:32.2.0')
implementation platform('com.google.firebase:firebase-bom:32.2.2')
implementation 'com.google.firebase:firebase-dynamic-links-ktx'
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'

119
app/proguard-rules.pro vendored
View File

@ -21,38 +21,76 @@
#-renamesourcefileattribute SourceFile
##---------------Begin: proguard configuration for Gson ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
### Gson ProGuard and R8 rules which are relevant for all users
### This file is automatically recognized by ProGuard and R8, see https://developer.android.com/build/shrink-code#configuration-files
###
### IMPORTANT:
### - These rules are additive; don't include anything here which is not specific to Gson (such as completely
### disabling obfuscation for all classes); the user would be unable to disable that then
### - These rules are not complete; users will most likely have to add additional rules for their specific
### classes, for example to disable obfuscation for certain fields or to keep no-args constructors
###
# Keep generic signatures; needed for correct type resolution
-keepattributes Signature
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Keep Gson annotations
# Note: Cannot perform finer selection here to only cover Gson annotations, see also https://stackoverflow.com/q/47515093
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
# Gson specific classes
-dontwarn sun.misc.**
#-keep class com.google.gson.stream.** { *; }
# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { <fields>; }
### The following rules are needed for R8 in "full mode" which only adheres to `-keepattribtues` if
### the corresponding class or field is matches by a `-keep` rule as well, see
### https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#r8-full-mode
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
# Keep class TypeToken (respectively its generic signature)
-keep class com.google.gson.reflect.TypeToken { *; }
# Prevent R8 from leaving Data object members always null
# Keep any (anonymous) classes extending TypeToken
-keep,allowobfuscation class * extends com.google.gson.reflect.TypeToken
# Keep classes with @JsonAdapter annotation
-keep,allowobfuscation,allowoptimization @com.google.gson.annotations.JsonAdapter class *
# Keep fields with @SerializedName annotation, but allow obfuscation of their names
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
@com.google.gson.annotations.SerializedName <fields>;
}
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
# Keep fields with any other Gson annotation
# Also allow obfuscation, assuming that users will additionally use @SerializedName or
# other means to preserve the field names
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.Expose <fields>;
@com.google.gson.annotations.JsonAdapter <fields>;
@com.google.gson.annotations.Since <fields>;
@com.google.gson.annotations.Until <fields>;
}
##---------------End: proguard configuration for Gson ----------
# Keep no-args constructor of classes which can be used with @JsonAdapter
# By default their no-args constructor is invoked to create an adapter instance
-keepclassmembers class * extends com.google.gson.TypeAdapter {
<init>();
}
-keepclassmembers class * implements com.google.gson.TypeAdapterFactory {
<init>();
}
-keepclassmembers class * implements com.google.gson.JsonSerializer {
<init>();
}
-keepclassmembers class * implements com.google.gson.JsonDeserializer {
<init>();
}
# If a class is used in some way by the application, and has fields annotated with @SerializedName
# and a no-args constructor, keep those fields and the constructor
# Based on https://issuetracker.google.com/issues/150189783#comment11
# See also https://github.com/google/gson/pull/2420#discussion_r1241813541 for a more detailed explanation
-if class *
-keepclasseswithmembers,allowobfuscation,allowoptimization class <1> {
<init>();
@com.google.gson.annotations.SerializedName <fields>;
}
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep class * extends com.bumptech.glide.module.AppGlideModule {
@ -112,30 +150,59 @@
@retrofit2.http.* <methods>;
}
# Ignore annotation used for build tooling.
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
# Ignore JSR 305 annotations for embedding nullability information.
-dontwarn javax.annotation.**
# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
-dontwarn kotlin.Unit
# Top-level functions that can only be used by Kotlin.
-dontwarn retrofit2.KotlinExtensions
-dontwarn retrofit2.KotlinExtensions$*
# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface <1>
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response
# Keep inherited services.
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface * extends <1>
# With R8 full mode generic signatures are stripped for classes that are not
# kept. Suspend functions are wrapped in continuations where the type argument
# is used.
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
-keepattributes Signature
-keep class kotlin.coroutines.Continuation
# R8 full mode strips generic signatures from return types if not kept.
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
# With R8 full mode generic signatures are stripped for classes that are not kept.
-keep,allowobfuscation,allowshrinking class retrofit2.Response
# Keep generic signature of RxJava3 (R8 full mode strips signatures from non-kept items).
-keep,allowobfuscation,allowshrinking class io.reactivex.rxjava3.core.Flowable
-keep,allowobfuscation,allowshrinking class io.reactivex.rxjava3.core.Maybe
-keep,allowobfuscation,allowshrinking class io.reactivex.rxjava3.core.Observable
-keep,allowobfuscation,allowshrinking class io.reactivex.rxjava3.core.Single
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
-keep,allowoptimization,allowshrinking,allowobfuscation enum <3>
-keep,allowoptimization,allowshrinking,allowobfuscation interface <3>
-dontwarn java.nio.file.Files
-dontwarn java.nio.file.Path
-dontwarn java.nio.file.OpenOption
-dontwarn com.google.devtools.build.android.desugar.runtime.ThrowableExtension
-keep class io.agora.**{*;}
-dontwarn org.codehaus.mojo.**
@ -152,3 +219,5 @@
-keep class androidx.recyclerview.widget.**{*;}
-keep class androidx.viewpager2.widget.**{*;}
-keep class kr.co.bootpay.core.** { *; }

View File

@ -50,6 +50,17 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="sodalive.page.link" />
<data android:host="sodalive.net" />
</intent-filter>
</activity>
<activity android:name=".main.MainActivity" />
<activity android:name=".user.login.LoginActivity" />
@ -62,7 +73,9 @@
<activity android:name=".live.room.create.LiveRoomCreateActivity" />
<activity android:name=".live.room.update.LiveRoomEditActivity" />
<activity android:name=".live.reservation.complete.LiveReservationCompleteActivity" />
<activity android:name=".live.room.LiveRoomActivity" />
<activity
android:name=".live.room.LiveRoomActivity"
android:windowSoftInputMode="stateAlwaysHidden|adjustPan" />
<activity android:name=".explorer.profile.UserProfileActivity" />
<activity android:name=".explorer.profile.donation.UserProfileDonationAllViewActivity" />
<activity android:name=".explorer.profile.fantalk.UserProfileFantalkAllViewActivity" />
@ -89,6 +102,10 @@
<activity android:name=".live.now.all.LiveNowAllActivity" />
<activity android:name=".live.reservation.all.LiveReservationAllActivity" />
<activity android:name=".mypage.service_center.ServiceCenterActivity" />
<activity android:name=".onboarding.OnBoardingActivity" />
<activity android:name=".mypage.profile.ProfileUpdateActivity" />
<activity android:name=".mypage.profile.nickname.NicknameUpdateActivity" />
<activity android:name=".mypage.profile.password.ModifyPasswordActivity" />
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"

View File

@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.audio_content
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
@ -40,19 +39,13 @@ class AudioContentAdapter(
} else {
binding.tvPrice.text = item.price.moneyFormat()
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_coin_w,
R.drawable.ic_can,
0,
0,
0
)
}
binding.iv19.visibility = if (item.isAdult) {
View.VISIBLE
} else {
View.GONE
}
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}

View File

@ -146,7 +146,7 @@ class AudioContentPlayService :
}
MusicAction.PLAY.name -> {
if (!isPlaying) {
if (!isPlaying && this::mediaPlayer.isInitialized) {
mediaPlayer.start()
toggleIsPlaying()
updateNotification()
@ -154,7 +154,7 @@ class AudioContentPlayService :
}
MusicAction.PAUSE.name -> {
if (isPlaying) {
if (isPlaying && this::mediaPlayer.isInitialized) {
mediaPlayer.pause()
toggleIsPlaying()
updateNotification()
@ -186,11 +186,14 @@ class AudioContentPlayService :
MusicAction.PROGRESS.name -> {
val progress = intent.getIntExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, 0)
if (progress > 0) {
if (contentId != null) saveNewPlaybackTracking(
totalDuration = mediaPlayer.duration,
progress = progress
)
if (progress > 0 && this::mediaPlayer.isInitialized) {
if (contentId != null) {
saveNewPlaybackTracking(
totalDuration = mediaPlayer.duration,
progress = progress
)
}
mediaPlayer.seekTo(progress)
}
}
@ -258,7 +261,7 @@ class AudioContentPlayService :
}
)
if (isPlaying) {
if (isPlaying && this::mediaPlayer.isInitialized) {
mediaPlayer.stop()
setEndPositionPlaybackTracking(mediaPlayer.currentPosition)
@ -290,12 +293,12 @@ class AudioContentPlayService :
return null
}
override fun onCompletion(mp: MediaPlayer?) {
setEndPositionPlaybackTracking(mediaPlayer.currentPosition)
override fun onCompletion(mp: MediaPlayer) {
setEndPositionPlaybackTracking(mp.currentPosition)
if (SharedPreferenceManager.isContentPlayLoop) {
saveNewPlaybackTracking(totalDuration = mediaPlayer.duration, progress = 0)
mediaPlayer.start()
saveNewPlaybackTracking(totalDuration = mp.duration, progress = 0)
mp.start()
} else {
toggleIsPlaying(false)
mediaPlayer.release()
@ -370,61 +373,63 @@ class AudioContentPlayService :
}
override fun onPrepared(mp: MediaPlayer?) {
saveNewPlaybackTracking(totalDuration = mediaPlayer.duration, progress = 0)
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PLAY
)
if (this::mediaPlayer.isInitialized) {
saveNewPlaybackTracking(totalDuration = mediaPlayer.duration, progress = 0)
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PLAY
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_DURATION,
mediaPlayer.duration
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_DURATION,
mediaPlayer.duration
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
contentId
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
contentId
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ALERT_PREVIEW,
true
)
}
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ALERT_PREVIEW,
true
)
}
)
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
false
)
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
false
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
true
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
true
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_TITLE,
title
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_TITLE,
title
)
putExtra(
Constants.EXTRA_NICKNAME,
nickname
)
putExtra(
Constants.EXTRA_NICKNAME,
nickname
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL,
coverImageUrl
)
}
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL,
coverImageUrl
)
}
)
}
}
private fun updateNotification() {
@ -463,7 +468,7 @@ class AudioContentPlayService :
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
val notificationBuilder = NotificationCompat
.Builder(this@AudioContentPlayService, channelId)
.setSmallIcon(R.drawable.ic_noti)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(resource)
.setContentTitle(title ?: "오디오 콘텐츠")
.setContentText(nickname ?: "")

View File

@ -30,34 +30,34 @@ class AudioContentCommentAdapter(
}
val tvCommentLayoutParams = binding.tvComment.layoutParams as LinearLayout.LayoutParams
val coin = item.donationCoin
if (coin > 0) {
val can = item.donationCan
if (can > 0) {
tvCommentLayoutParams.topMargin = 0
binding.llDonationCoin.visibility = View.VISIBLE
binding.tvDonationCoin.text = coin.moneyFormat()
binding.llDonationCoin.setBackgroundResource(
binding.llDonationCan.visibility = View.VISIBLE
binding.tvDonationCan.text = can.moneyFormat()
binding.llDonationCan.setBackgroundResource(
when {
coin >= 100000 -> {
can >= 100000 -> {
R.drawable.bg_round_corner_10_7_973a3a
}
coin >= 50000 -> {
can >= 50000 -> {
R.drawable.bg_round_corner_10_7_d85e37
}
coin >= 10000 -> {
can >= 10000 -> {
R.drawable.bg_round_corner_10_7_d38c38
}
coin >= 5000 -> {
can >= 5000 -> {
R.drawable.bg_round_corner_10_7_59548f
}
coin >= 1000 -> {
can >= 1000 -> {
R.drawable.bg_round_corner_10_7_4d6aa4
}
coin >= 500 -> {
can >= 500 -> {
R.drawable.bg_round_corner_10_7_2d7390
}
@ -68,7 +68,7 @@ class AudioContentCommentAdapter(
)
} else {
tvCommentLayoutParams.topMargin = 13.3f.dpToPx().toInt()
binding.llDonationCoin.visibility = View.GONE
binding.llDonationCan.visibility = View.GONE
}
binding.tvComment.layoutParams = tvCommentLayoutParams

View File

@ -63,7 +63,7 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}

View File

@ -63,7 +63,7 @@ class AudioContentCommentReplyHeaderViewHolder(
override fun bind(item: GetAudioContentCommentListItem) {
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
@ -82,7 +82,7 @@ class AudioContentCommentReplyItemViewHolder(
override fun bind(item: GetAudioContentCommentListItem) {
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}

View File

@ -83,7 +83,7 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}

View File

@ -15,7 +15,7 @@ data class GetAudioContentCommentListItem(
@SerializedName("nickname") val nickname: String,
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("comment") val comment: String,
@SerializedName("donationCoin") val donationCoin: Int,
@SerializedName("donationCan") val donationCan: Int,
@SerializedName("date") val date: String,
@SerializedName("replyCount") val replyCount: Int
) : Parcelable

View File

@ -248,7 +248,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
LayoutInflater.from(this)
) { can, message ->
if (can <= 0) {
showToast("1코인 이상 후원하실 수 있습니다.")
showToast("1 이상 후원하실 수 있습니다.")
} else if (message.isBlank()) {
showToast("함께 보낼 메시지를 입력하세요.")
} else {
@ -260,8 +260,8 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
}
private fun donation(coin: Int, message: String) {
viewModel.donation(audioContentId, coin, message) {
private fun donation(can: Int, message: String) {
viewModel.donation(audioContentId, can, message) {
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
}
@ -693,7 +693,6 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
title = audioContent.title,
theme = audioContent.themeStr,
coverImageUrl = audioContent.coverImageUrl,
isAdult = audioContent.isAdult,
profileImageUrl = audioContent.creator.profileImageUrl,
nickname = audioContent.creator.nickname,
duration = audioContent.duration,

View File

@ -332,15 +332,15 @@ class AudioContentDetailViewModel(
) {
isLoading.value = true
Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) {
link = Uri.parse("https://yozm.day/?audio_content_id=$audioContentId")
domainUriPrefix = "https://yozm.page.link"
link = Uri.parse("https://sodalive.net/?audio_content_id=$audioContentId")
domainUriPrefix = "https://sodalive.page.link"
androidParameters { }
iosParameters("kr.co.vividnext.yozm") {
appStoreId = "1630284226"
iosParameters("kr.co.vividnext.sodalive") {
appStoreId = "6461721697"
}
socialMetaTagParameters {
title = contentTitle
description = "지금 요즘라이브에서 이 콘텐츠 감상하기"
description = "지금 소다라이브에서 이 콘텐츠 감상하기"
imageUrl = contentImage.toUri()
}
}.addOnSuccessListener {

View File

@ -1,15 +1,19 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.widget.FrameLayout
import android.widget.ImageView
import coil.load
import coil.transform.RoundedCornersTransformation
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.bannerview.BaseViewHolder
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainBannerAdapter(
private val context: Context,
private val itemWidth: Int,
private val itemHeight: Int,
private val onClick: (GetAudioContentBannerResponse) -> Unit
@ -26,12 +30,20 @@ class AudioContentMainBannerAdapter(
layoutParams.width = itemWidth
layoutParams.height = itemHeight
ivBanner.load(data.thumbnailImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
ivBanner.layoutParams = layoutParams
Glide
.with(context)
.asBitmap()
.load(data.thumbnailImageUrl)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
ivBanner.setImageBitmap(resource)
ivBanner.layoutParams = layoutParams
}
override fun onLoadCleared(placeholder: Drawable?) {
}
})
ivBanner.setOnClickListener { onClick(data) }
}

View File

@ -142,7 +142,11 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
layoutParams.width = pagerWidth.roundToInt()
layoutParams.height = pagerHeight
bannerAdapter = AudioContentMainBannerAdapter(pagerWidth.roundToInt(), pagerHeight) {
bannerAdapter = AudioContentMainBannerAdapter(
requireContext(),
pagerWidth.roundToInt(),
pagerHeight
) {
when (it.type) {
AudioContentBannerType.EVENT -> {
startActivity(

View File

@ -1,6 +1,5 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
@ -17,13 +16,13 @@ class AudioContentMainItemViewHolder(
fun bind(item: GetAudioContentMainItem) {
binding.ivAudioContentCoverImage.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(2.7f.dpToPx()))
}
binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
@ -31,11 +30,6 @@ class AudioContentMainItemViewHolder(
binding.tvAudioContentCreatorNickname.text = item.creatorNickname
binding.ivAudioContentCreator.setOnClickListener { onClickCreator(item.creatorId) }
binding.iv19.visibility = if (item.isAdult) {
View.VISIBLE
} else {
View.GONE
}
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}

View File

@ -22,7 +22,7 @@ class AudioContentMainNewContentCreatorAdapter(
binding.tvNewContentCreator.text = item.creatorNickname
binding.ivNewContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.root.setOnClickListener { onClickItem(item.creatorId) }

View File

@ -32,7 +32,7 @@ data class GetAudioContentMainItem(
data class GetAudioContentCurationResponse(
@SerializedName("title") val title: String,
@SerializedName("description") val description: String,
@SerializedName("audioContents") val audioContents: List<GetAudioContentMainItem>
@SerializedName("contents") val audioContents: List<GetAudioContentMainItem>
)
data class GetAudioContentBannerResponse(

View File

@ -4,7 +4,6 @@ import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import coil.load
@ -21,7 +20,6 @@ class AudioContentOrderConfirmDialog(
title: String,
theme: String,
coverImageUrl: String,
isAdult: Boolean,
profileImageUrl: String,
nickname: String,
duration: String,
@ -48,33 +46,27 @@ class AudioContentOrderConfirmDialog(
dialogView.ivCover.load(coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4f))
}
dialogView.ivProfile.load(profileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
dialogView.tvDuration.text = duration
dialogView.tvPrice.text = if (orderType == OrderType.RENTAL) {
"${ceil(price * 0.7).toInt()}"
"${ceil(price * 0.6).toInt()}"
} else {
"$price"
}
dialogView.iv19.visibility = if (isAdult) {
View.VISIBLE
} else {
View.GONE
}
dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
"콘텐츠를 대여하시겠습니까?\n아래 코인이 차감됩니다."
"콘텐츠를 대여하시겠습니까?\n아래 캔이 차감됩니다."
} else {
"콘텐츠를 소장하시겠습니까?\n아래 코인이 차감됩니다."
"콘텐츠를 소장하시겠습니까?\n아래 캔이 차감됩니다."
}
dialogView.tvCancel.setOnClickListener {

View File

@ -29,7 +29,7 @@ class AudioContentOrderFragment(
super.onViewCreated(view, savedInstanceState)
binding.tvKeep.text = "$price"
binding.tvRental.text = "${ceil(price * 0.7).toInt()}"
binding.tvRental.text = "${ceil(price * 0.6).toInt()}"
binding.llKeep.setOnClickListener {
onClickKeep()

View File

@ -23,7 +23,7 @@ class AudioContentOrderListAdapter(
fun bind(item: GetAudioContentOrderListItem) {
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}

View File

@ -70,7 +70,7 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
binding.ivCover.background = null
binding.ivCover.load(fileUri) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
viewModel.coverImageUri = fileUri

View File

@ -211,8 +211,8 @@ class AudioContentUploadViewModel(
return false
}
if (!isPriceFreeLiveData.value!! && price < 10) {
_toastLiveData.postValue("콘텐츠의 최소금액은 10코인 입니다.")
if (!isPriceFreeLiveData.value!! && price < 5) {
_toastLiveData.postValue("콘텐츠의 최소금액은 5캔 입니다.")
return false
}

View File

@ -35,7 +35,7 @@ class AudioContentThemeAdapter(
binding.ivTheme.load(item.image) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}

View File

@ -13,6 +13,7 @@ object Constants {
const val PREF_IS_CONTENT_PLAY_LOOP = "pref_is_content_play_loop"
const val PREF_IS_FOLLOWED_CREATOR_LIVE = "pref_is_followed_creator_live"
const val PREF_NOT_SHOWING_EVENT_POPUP_ID = "pref_not_showing_event_popup_id"
const val PREF_IS_VIEWED_ON_BOARDING_TUTORIAL = "pref_is_viewed_on_boarding_tutorial"
const val EXTRA_CAN = "extra_can"
const val EXTRA_DATA = "extra_data"

View File

@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.common
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.AnimationDrawable
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.WindowManager
@ -16,14 +15,11 @@ class LoadingDialog(
) {
private val alertDialog: AlertDialog
private val dialogView = DialogLoadingBinding.inflate(layoutInflater)
private val animationDrawable: AnimationDrawable
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
animationDrawable = dialogView.tvLoading.compoundDrawables[1] as AnimationDrawable
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
@ -31,7 +27,6 @@ class LoadingDialog(
fun show(width: Int, message: String = "") {
alertDialog.show()
animationDrawable.start()
dialogView.tvLoading.text = message
val lp = WindowManager.LayoutParams()
@ -43,7 +38,6 @@ class LoadingDialog(
}
fun dismiss() {
animationDrawable.stop()
alertDialog.dismiss()
}
}

View File

@ -116,4 +116,10 @@ object SharedPreferenceManager {
set(value) {
sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID] = value
}
var isViewedOnboardingTutorial: Boolean
get() = sharedPreferences[Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL, false]
set(value) {
sharedPreferences[Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL] = value
}
}

View File

@ -62,7 +62,7 @@ class SodaLiveService : Service() {
)
val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId)
.setSmallIcon(R.drawable.ic_noti)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(getString(R.string.app_name))
.setContentText(content)
.setOngoing(true)

View File

@ -22,6 +22,8 @@ import kr.co.vividnext.sodalive.explorer.ExplorerApi
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.explorer.ExplorerViewModel
import kr.co.vividnext.sodalive.explorer.profile.UserProfileViewModel
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAllViewModel
import kr.co.vividnext.sodalive.explorer.profile.fantalk.UserProfileFantalkAllViewModel
import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListViewModel
import kr.co.vividnext.sodalive.following.FollowingCreatorRepository
import kr.co.vividnext.sodalive.following.FollowingCreatorViewModel
@ -55,6 +57,11 @@ import kr.co.vividnext.sodalive.mypage.can.CanRepository
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeViewModel
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentViewModel
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusViewModel
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateViewModel
import kr.co.vividnext.sodalive.mypage.profile.nickname.NicknameUpdateViewModel
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagApi
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagRepository
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagViewModel
import kr.co.vividnext.sodalive.mypage.service_center.FaqApi
import kr.co.vividnext.sodalive.mypage.service_center.FaqRepository
import kr.co.vividnext.sodalive.mypage.service_center.ServiceCenterViewModel
@ -134,6 +141,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
single { ApiBuilder().build(get(), NoticeApi::class.java) }
single { ApiBuilder().build(get(), AudioContentApi::class.java) }
single { ApiBuilder().build(get(), FaqApi::class.java) }
single { ApiBuilder().build(get(), MemberTagApi::class.java) }
}
private val viewModelModule = module {
@ -179,6 +187,10 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { AudioContentCommentReplyViewModel(get()) }
viewModel { FollowingCreatorViewModel(get()) }
viewModel { ServiceCenterViewModel(get()) }
viewModel { ProfileUpdateViewModel(get()) }
viewModel { NicknameUpdateViewModel(get()) }
viewModel { MemberTagViewModel(get()) }
viewModel { UserProfileDonationAllViewModel(get()) }
}
private val repositoryModule = module {
@ -199,6 +211,8 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
factory { PlaybackTrackingRepository(get()) }
factory { FollowingCreatorRepository(get(), get()) }
factory { FaqRepository(get()) }
factory { MemberTagRepository(get()) }
factory { UserProfileFantalkAllViewModel(get(), get()) }
}
private val moduleList = listOf(

View File

@ -1,11 +1,14 @@
package kr.co.vividnext.sodalive.explorer
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.GetCheersResponse
import kr.co.vividnext.sodalive.explorer.profile.GetCreatorProfileResponse
import kr.co.vividnext.sodalive.explorer.profile.PostCreatorNoticeRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.donation.GetDonationAllResponse
import kr.co.vividnext.sodalive.explorer.profile.follow.GetFollowerListResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import retrofit2.http.Body
@ -35,6 +38,23 @@ interface ExplorerApi {
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetCreatorProfileResponse>>
@GET("/explorer/profile/{id}/donation-rank")
fun getCreatorProfileDonationRanking(
@Path("id") id: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetDonationAllResponse>>
@GET("/explorer/profile/{id}/cheers")
fun getCreatorProfileCheers(
@Path("id") creatorId: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Flowable<ApiResponse<GetCheersResponse>>
@POST("/explorer/profile/cheers")
fun writeCheers(
@Body request: PostWriteCheersRequest,

View File

@ -1,8 +1,13 @@
package kr.co.vividnext.sodalive.explorer
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.GetCheersResponse
import kr.co.vividnext.sodalive.explorer.profile.PostCreatorNoticeRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.donation.GetDonationAllResponse
import java.util.TimeZone
class ExplorerRepository(
@ -21,6 +26,21 @@ class ExplorerRepository(
authHeader = token
)
fun getCreatorProfileCheers(
creatorId: Long,
page: Int,
size: Int,
token: String
): Flowable<ApiResponse<GetCheersResponse>> {
return api.getCreatorProfileCheers(
creatorId = creatorId,
page = page - 1,
size = size,
timezone = TimeZone.getDefault().id,
authHeader = token
)
}
fun writeCheers(
parentCheersId: Long?,
creatorId: Long,
@ -63,4 +83,18 @@ class ExplorerRepository(
size = size,
authHeader = token
)
fun getCreatorProfileDonationRanking(
id: Long,
page: Int,
size: Int,
token: String
): Single<ApiResponse<GetDonationAllResponse>> {
return api.getCreatorProfileDonationRanking(
id = id,
page = page - 1,
size = size,
authHeader = token
)
}
}

View File

@ -24,7 +24,7 @@ class ExplorerSectionAdapter(
binding.ivProfile.load(item.profileImageUrl) {
transformations(CircleCropTransformation())
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
crossfade(true)
}

View File

@ -11,8 +11,8 @@ data class GetCreatorProfileResponse(
val similarCreatorList: List<SimilarCreatorResponse>,
@SerializedName("liveRoomList")
val liveRoomList: List<LiveRoomResponse>,
@SerializedName("audioContentList")
val audioContentList: List<GetAudioContentListItem>,
@SerializedName("contentList")
val contentList: List<GetAudioContentListItem>,
@SerializedName("notice")
val notice: String,
@SerializedName("cheers")
@ -42,7 +42,7 @@ data class UserDonationRankingResponse(
@SerializedName("userId") val userId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImage") val profileImage: String,
@SerializedName("donationCoin") val donationCoin: Int
@SerializedName("donationCan") val donationCan: Int
)
data class SimilarCreatorResponse(

View File

@ -23,6 +23,10 @@ import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentActivity
import kr.co.vividnext.sodalive.audio_content.AudioContentAdapter
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
@ -56,6 +60,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
private lateinit var liveAdapter: UserProfileLiveAdapter
private lateinit var audioContentAdapter: AudioContentAdapter
private lateinit var donationAdapter: UserProfileDonationAdapter
private lateinit var similarCreatorAdapter: UserProfileSimilarCreatorAdapter
private lateinit var cheersAdapter: UserProfileCheersAdapter
@ -118,6 +123,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
setupDonationView()
setupSimilarCreatorView()
setupFanTalkView()
setupAudioContentListView()
}
private fun hideKeyboard(onAfterExecute: () -> Unit) {
@ -432,6 +438,52 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
rvCheers.adapter = cheersAdapter
}
private fun setupAudioContentListView() {
binding.layoutUserProfileAudioContent.tvAll.setOnClickListener {
val intent = Intent(applicationContext, AudioContentActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, userId)
startActivity(intent)
}
val recyclerView = binding.layoutUserProfileAudioContent.rvAudioContent
audioContentAdapter = AudioContentAdapter {
val intent = Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
startActivity(intent)
}
recyclerView.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
audioContentAdapter.itemCount - 1 -> {
outRect.bottom = 0
}
else -> {
outRect.bottom = 13.3f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = audioContentAdapter
}
private fun showCheersReportPopup(cheersId: Long) {
val dialog = CheersReportDialog(this, layoutInflater) {
if (it.isBlank()) {
@ -477,6 +529,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
setCheers(it.cheers)
setCreatorProfile(it.creator)
setCreatorNotice(it.notice, it.creator.creatorId)
setAudioContentList(it.contentList)
setLiveRoomList(it.liveRoomList)
setSimilarCreatorList(it.similarCreatorList)
setUserDonationRanking(it.userDonationRanking)
@ -536,7 +589,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
layoutUserProfile.ivProfile.load(creator.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
@ -625,6 +678,36 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setAudioContentList(audioContentList: List<GetAudioContentListItem>) {
binding.layoutUserProfileAudioContent.root.visibility =
if (userId == SharedPreferenceManager.userId || audioContentList.isNotEmpty()) {
View.VISIBLE
} else {
View.GONE
}
if (userId == SharedPreferenceManager.userId) {
binding.layoutUserProfileAudioContent.tvTitle.text = "내 콘텐츠"
binding.layoutUserProfileAudioContent.tvNewContent.setOnClickListener {
startActivity(
Intent(
applicationContext,
AudioContentUploadActivity::class.java
)
)
}
binding.layoutUserProfileAudioContent.tvNewContent.visibility = View.VISIBLE
} else {
binding.layoutUserProfileAudioContent.tvTitle.text = "콘텐츠"
binding.layoutUserProfileAudioContent.tvNewContent.visibility = View.GONE
}
audioContentAdapter.items.clear()
audioContentAdapter.items.addAll(audioContentList)
audioContentAdapter.notifyDataSetChanged()
}
@SuppressLint("NotifyDataSetChanged")
private fun setLiveRoomList(liveRoomList: List<LiveRoomResponse>) {
if (liveRoomList.isEmpty()) {

View File

@ -28,16 +28,10 @@ class UserProfileLiveAdapter(
fun bind(item: LiveRoomResponse) {
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
}
binding.iv19.visibility = if (item.isAdult) {
View.VISIBLE
} else {
View.GONE
}
binding.tvDate.text = item.beginDateTime
binding.tvNickname.text = item.managerNickname
binding.tvTitle.text = item.title

View File

@ -20,7 +20,7 @@ class UserProfileSimilarCreatorAdapter(
fun bind(item: SimilarCreatorResponse) {
binding.ivProfile.load(item.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}

View File

@ -295,16 +295,16 @@ class UserProfileViewModel(
fun shareChannel(userId: Long, onSuccess: (String) -> Unit) {
_isLoading.value = true
Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) {
link = Uri.parse("https://yozm.day/?channel_id=$userId")
domainUriPrefix = "https://yozm.page.link"
link = Uri.parse("https://sodalive.net/?channel_id=$userId")
domainUriPrefix = "https://sodalive.page.link"
androidParameters { }
iosParameters("kr.co.vividnext.yozm") {
appStoreId = "1630284226"
iosParameters("kr.co.vividnext.sodalive") {
appStoreId = "6461721697"
}
}.addOnSuccessListener {
val uri = it.shortLink
if (uri != null) {
onSuccess("요즘라이브 ${creatorNickname}님의 채널입니다.\n$uri")
onSuccess("소다라이브 ${creatorNickname}님의 채널입니다.\n$uri")
}
}.addOnFailureListener {
_toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.")

View File

@ -35,7 +35,7 @@ class UserProfileCheersAdapter(
binding.ivProfile.load(cheers.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(16.7f.dpToPx()))
}
binding.tvContent.text = cheers.content

View File

@ -0,0 +1,17 @@
package kr.co.vividnext.sodalive.explorer.profile.donation
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.explorer.profile.UserDonationRankingResponse
data class GetDonationAllResponse(
@SerializedName("accumulatedCansToday")
val accumulatedCansToday: Int,
@SerializedName("accumulatedCansLastWeek")
val accumulatedCansLastWeek: Int,
@SerializedName("accumulatedCansThisMonth")
val accumulatedCansThisMonth: Int,
@SerializedName("totalCount")
val totalCount: Int,
@SerializedName("userDonationRanking")
val userDonationRanking: List<UserDonationRankingResponse>,
)

View File

@ -22,7 +22,7 @@ class UserProfileDonationAdapter : RecyclerView.Adapter<UserProfileDonationAdapt
binding.ivProfile.load(item.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}

View File

@ -0,0 +1,154 @@
package kr.co.vividnext.sodalive.explorer.profile.donation
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ItemUserProfileDonationAllBinding
import kr.co.vividnext.sodalive.explorer.profile.UserDonationRankingResponse
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class UserProfileDonationAllAdapter(private val userId: Long) :
RecyclerView.Adapter<UserProfileDonationAllAdapter.ViewHolder>() {
val items = mutableListOf<UserDonationRankingResponse>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemUserProfileDonationAllBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(item: UserDonationRankingResponse, position: Int) {
binding.tvRank.text = "${position + 1}"
binding.tvNickname.text = item.nickname
binding.ivProfile.load(item.profileImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
if (item.donationCan > 0 && SharedPreferenceManager.userId == userId) {
binding.tvTotalDonationCan.visibility = View.VISIBLE
binding.tvTotalDonationCan.text = "${item.donationCan.moneyFormat()}"
}
val lp = binding.rlDonationRanking.layoutParams as RelativeLayout.LayoutParams
when (position) {
0 -> {
binding.ivBg.setImageResource(R.drawable.bg_circle_ffdc00_ffb600)
binding.ivBg.visibility = View.VISIBLE
binding.ivCrown.setImageResource(R.drawable.ic_crown_1)
binding.ivCrown.visibility = View.VISIBLE
binding.rlDonationRankingRoot.setBackgroundResource(
if (items.size == 1) {
R.drawable.bg_round_corner_4_7_2b2635
} else {
R.drawable.bg_top_round_corner_4_7_2b2635
}
)
lp.setMargins(
20.dpToPx().toInt(),
20.dpToPx().toInt(),
20.dpToPx().toInt(),
if (items.size == 1) {
20.dpToPx().toInt()
} else {
13.3f.dpToPx().toInt()
}
)
binding.rlDonationRanking.layoutParams = lp
}
1 -> {
binding.ivBg.setImageResource(R.drawable.bg_circle_ffffff_9f9f9f)
binding.ivBg.visibility = View.VISIBLE
binding.ivCrown.setImageResource(R.drawable.ic_crown_2)
binding.ivCrown.visibility = View.VISIBLE
if (items.size == 2) {
binding.rlDonationRankingRoot.setBackgroundResource(
R.drawable.bg_bottom_round_corner_4_7_2b2635
)
} else {
binding.rlDonationRankingRoot.setBackgroundColor(
ContextCompat.getColor(context, R.color.color_2b2635)
)
}
lp.setMargins(
20.dpToPx().toInt(),
0,
20.dpToPx().toInt(),
if (items.size == 2) {
20.dpToPx().toInt()
} else {
13.3f.dpToPx().toInt()
}
)
binding.rlDonationRanking.layoutParams = lp
}
2 -> {
binding.ivBg.setImageResource(R.drawable.bg_circle_e6a77a_c67e4a)
binding.ivBg.visibility = View.VISIBLE
binding.ivCrown.setImageResource(R.drawable.ic_crown_3)
binding.ivCrown.visibility = View.VISIBLE
binding.rlDonationRankingRoot.setBackgroundResource(
R.drawable.bg_bottom_round_corner_4_7_2b2635
)
lp.setMargins(
20.dpToPx().toInt(),
0,
20.dpToPx().toInt(),
20.dpToPx().toInt()
)
binding.rlDonationRanking.layoutParams = lp
}
else -> {
binding.ivBg.setImageResource(0)
binding.ivBg.visibility = View.GONE
binding.ivCrown.visibility = View.GONE
binding.rlDonationRanking.setBackgroundResource(0)
binding.rlDonationRanking.background = null
binding.rlDonationRankingRoot.setBackgroundResource(0)
binding.rlDonationRankingRoot.background = null
lp.setMargins(0, 0, 0, 0)
binding.rlDonationRanking.layoutParams = lp
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemUserProfileDonationAllBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], position)
}
override fun getItemCount() = items.count()
}

View File

@ -1,10 +1,149 @@
package kr.co.vividnext.sodalive.explorer.profile.donation
import android.annotation.SuppressLint
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityUserProfileLiveAllBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
import org.koin.android.ext.android.inject
class UserProfileDonationAllViewActivity : BaseActivity<ActivityUserProfileLiveAllBinding>(
ActivityUserProfileLiveAllBinding::inflate
) {
override fun setupView() {}
private val viewModel: UserProfileDonationAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: UserProfileDonationAllAdapter
private var userId: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
super.onCreate(savedInstanceState)
if (userId > 0) {
bindData()
viewModel.getCreatorProfileDonationRanking(userId)
} else {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "후원랭킹 전체보기"
binding.toolbar.tvBack.setOnClickListener { finish() }
setupDonationRankingView()
}
private fun setupDonationRankingView() {
val recyclerView = binding.rvLiveAll
adapter = UserProfileDonationAllAdapter(userId = userId)
recyclerView.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 20.dpToPx().toInt()
outRect.right = 20.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0, 1, 2 -> {
outRect.top = 0
outRect.bottom = 0
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
}
3 -> {
outRect.top = 20.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = adapter
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisiblePosition = (recyclerView.layoutManager as LinearLayoutManager)
.findLastVisibleItemPosition()
val itemTotalCount = adapter.itemCount - 1
if (itemTotalCount > 0 && lastVisiblePosition == itemTotalCount) {
viewModel.getCreatorProfileDonationRanking(userId)
}
}
})
binding.swipeRefreshLayout.setOnRefreshListener {
adapter.items.clear()
viewModel.refresh(userId)
binding.swipeRefreshLayout.isRefreshing = false
}
if (SharedPreferenceManager.userId == userId) {
binding.llTotal.visibility = View.VISIBLE
} else {
binding.llTotal.visibility = View.GONE
}
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
}
}
viewModel.donationLiveData.observe(this) {
binding.tvCanToday.text = it.accumulatedCansToday.moneyFormat()
binding.tvCanLastWeek.text = it.accumulatedCansLastWeek.moneyFormat()
binding.tvCanThisMonth.text = it.accumulatedCansThisMonth.moneyFormat()
binding.tvTotalCount.text = "${it.totalCount}"
adapter.items.addAll(it.userDonationRanking)
adapter.notifyDataSetChanged()
}
}
}

View File

@ -0,0 +1,81 @@
package kr.co.vividnext.sodalive.explorer.profile.donation
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
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
class UserProfileDonationAllViewModel(
private val repository: ExplorerRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _donationLiveData = MutableLiveData<GetDonationAllResponse>()
val donationLiveData: LiveData<GetDonationAllResponse>
get() = _donationLiveData
private var isLast = false
private var page = 1
private val size = 10
fun getCreatorProfileDonationRanking(userId: Long) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getCreatorProfileDonationRanking(
id = userId,
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.userDonationRanking.isNotEmpty()) {
page += 1
_donationLiveData.postValue(it.data!!)
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
fun refresh(userId: Long) {
page = 1
isLast = false
getCreatorProfileDonationRanking(userId)
}
}

View File

@ -1,10 +1,203 @@
package kr.co.vividnext.sodalive.explorer.profile.fantalk
import android.annotation.SuppressLint
import android.app.Service
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.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.ActivityUserProfileFantalkAllBinding
import kr.co.vividnext.sodalive.explorer.profile.cheers.UserProfileCheersAdapter
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.report.CheersReportDialog
import org.koin.android.ext.android.inject
class UserProfileFantalkAllViewActivity : BaseActivity<ActivityUserProfileFantalkAllBinding>(
ActivityUserProfileFantalkAllBinding::inflate
) {
override fun setupView() {}
private val viewModel: UserProfileFantalkAllViewModel by inject()
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
private lateinit var cheersAdapter: UserProfileCheersAdapter
private val handler = Handler(Looper.getMainLooper())
private var userId: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
super.onCreate(savedInstanceState)
imm = getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager
if (userId > 0) {
bindData()
viewModel.getCheersList(creatorId = userId)
} else {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
}
@SuppressLint("SetTextI18n")
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "팬 Talk 전체보기"
binding.toolbar.tvBack.setOnClickListener { finish() }
setupCheersView()
}
private fun setupCheersView() {
binding.ivSend.setOnClickListener {
hideKeyboard {
viewModel.writeCheers(
creatorId = userId,
cheersContent = binding.etCheer.text.toString()
)
}
}
val rvCheers = binding.rvCheers
cheersAdapter = UserProfileCheersAdapter(
userId = userId,
enterReply = { cheersId, content ->
hideKeyboard {
viewModel.writeCheers(
parentCheersId = cheersId,
creatorId = userId,
cheersContent = content
)
}
},
modifyReply = { cheersId, content ->
hideKeyboard {
viewModel.modifyCheers(
cheersId = cheersId,
creatorId = userId,
cheersContent = content
)
}
},
onClickReport = { showCheersReportPopup(it) }
)
rvCheers.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
rvCheers.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.bottom = 0
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 0
}
cheersAdapter.itemCount - 1 -> {
outRect.top = 10.dpToPx().toInt()
outRect.bottom = 10.dpToPx().toInt()
}
else -> {
outRect.top = 10.dpToPx().toInt()
}
}
}
})
rvCheers.adapter = cheersAdapter
rvCheers.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisiblePosition = (recyclerView.layoutManager as LinearLayoutManager)
.findLastVisibleItemPosition()
val itemTotalCount = cheersAdapter.itemCount - 1
if (itemTotalCount > 0 && lastVisiblePosition == itemTotalCount) {
viewModel.getCheersList(userId)
}
}
})
}
private fun hideKeyboard(onAfterExecute: () -> Unit) {
handler.postDelayed({
imm.hideSoftInputFromWindow(
window.decorView.applicationWindowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
onAfterExecute()
}, 100)
}
private fun showCheersReportPopup(cheersId: Long) {
val dialog = CheersReportDialog(this, layoutInflater) {
if (it.isBlank()) {
Toast.makeText(
applicationContext,
"신고 이유를 선택해 주세요.",
Toast.LENGTH_LONG
).show()
} else {
viewModel.cheersReport(cheersId, reason = it)
}
}
dialog.show(screenWidth)
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
}
}
viewModel.cheersLiveData.observe(this) {
binding.etCheer.setText("")
binding.tvCheersCount.text = it.totalCount.toString()
binding.tvCheersCount.requestLayout()
cheersAdapter.items.addAll(it.cheers)
cheersAdapter.notifyDataSetChanged()
if (cheersAdapter.itemCount <= 0) {
binding.rvCheers.visibility = View.GONE
binding.tvNoCheers.visibility = View.VISIBLE
} else {
binding.rvCheers.visibility = View.VISIBLE
binding.tvNoCheers.visibility = View.GONE
}
binding.rvCheers.requestLayout()
}
}
}

View File

@ -0,0 +1,186 @@
package kr.co.vividnext.sodalive.explorer.profile.fantalk
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
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.explorer.profile.GetCheersResponse
import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.report.ReportRequest
import kr.co.vividnext.sodalive.report.ReportType
class UserProfileFantalkAllViewModel(
private val repository: ExplorerRepository,
private val reportRepository: ReportRepository
) : BaseViewModel() {
var cheersPage = 1
val pageSize = 10
private var isCheersLast = false
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _cheersLiveData = MutableLiveData<GetCheersResponse>()
val cheersLiveData: LiveData<GetCheersResponse>
get() = _cheersLiveData
fun getCheersList(creatorId: Long) {
if (!isCheersLast && !_isLoading.value!!) {
_isLoading.value = true
compositeDisposable.add(
repository.getCreatorProfileCheers(
creatorId = creatorId,
page = cheersPage,
size = pageSize,
token = "Bearer ${SharedPreferenceManager.token}"
).subscribe(
{
if (it.success && it.data != null) {
_cheersLiveData.postValue(it.data!!)
if (it.data.cheers.isNotEmpty()) {
cheersPage += 1
} else {
isCheersLast = true
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.postValue(false)
},
{
_isLoading.postValue(false)
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
fun cheersReport(cheersId: Long, reason: String) {
_isLoading.value = true
val request = ReportRequest(ReportType.CHEERS, reason, cheersId = cheersId)
compositeDisposable.add(
reportRepository.report(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"신고가 접수되었습니다."
)
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("신고가 접수되었습니다.")
}
)
)
}
fun writeCheers(parentCheersId: Long? = null, creatorId: Long, cheersContent: String) {
if (cheersContent.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
return
}
_isLoading.value = true
compositeDisposable.add(
repository.writeCheers(
parentCheersId = parentCheersId,
creatorId = creatorId,
content = cheersContent,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
isCheersLast = false
cheersPage = 1
getCheersList(creatorId)
} else {
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun modifyCheers(cheersId: Long, creatorId: Long, cheersContent: String) {
if (cheersContent.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
return
}
_isLoading.value = true
compositeDisposable.add(
repository.modifyCheers(
cheersId = cheersId,
content = cheersContent,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
isCheersLast = false
cheersPage = 1
getCheersList(creatorId)
} else {
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -24,7 +24,7 @@ class UserFollowerListAdapter(
binding.tvNickname.text = item.nickname
binding.ivProfile.load(item.profileImage) {
transformations(CircleCropTransformation())
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
crossfade(true)
}

View File

@ -56,9 +56,9 @@ class SodaFirebaseMessagingService : FirebaseMessagingService() {
intent.putExtra(Constants.EXTRA_ROOM_ID, roomId.toLong())
}
val socdocId = messageData["message_id"]
if (socdocId != null) {
intent.putExtra(Constants.EXTRA_MESSAGE_ID, socdocId.toLong())
val messageId = messageData["message_id"]
if (messageId != null) {
intent.putExtra(Constants.EXTRA_MESSAGE_ID, messageId.toLong())
}
val audioContentId = messageData["content_id"]
@ -66,6 +66,11 @@ class SodaFirebaseMessagingService : FirebaseMessagingService() {
intent.putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId.toLong())
}
val channelId = messageData["channel_id"]
if (channelId != null) {
intent.putExtra(Constants.EXTRA_USER_ID, channelId.toLong())
}
val pendingIntent =
PendingIntent.getActivity(
this,
@ -75,7 +80,7 @@ class SodaFirebaseMessagingService : FirebaseMessagingService() {
)
val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(messageData["title"])
.setContentText(messageData["message"])
.setSound(defaultSoundUri)

View File

@ -14,8 +14,8 @@ data class GetRoomListResponse(
@SerializedName("price") val price: Int,
@SerializedName("tags") val tags: List<String>,
@SerializedName("channelName") val channelName: String?,
@SerializedName("managerNickname") val managerNickname: String,
@SerializedName("managerId") val managerId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("isReservation") val isReservation: Boolean,
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean
)

View File

@ -9,7 +9,6 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.webkit.URLUtil
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
@ -22,7 +21,6 @@ import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle
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.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
@ -48,6 +46,7 @@ import kr.co.vividnext.sodalive.live.room.dialog.LiveCancelDialog
import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog
import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog
import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditActivity
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.notification.MemberRole
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
@ -95,25 +94,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
message = "라이브를 불러오고 있습니다."
viewModel.getSummary()
try {
val roomId = requireArguments().getLong(Constants.EXTRA_ROOM_ID)
val channelId = requireArguments().getLong(Constants.EXTRA_USER_ID)
val audioContentId = requireArguments().getLong(Constants.EXTRA_AUDIO_CONTENT_ID)
if (roomId > 0) {
enterLiveRoom(roomId)
} else if (channelId > 0) {
val nextIntent = Intent(requireContext(), UserProfileActivity::class.java)
nextIntent.putExtra(Constants.EXTRA_USER_ID, channelId)
startActivity(nextIntent)
} else if (audioContentId > 0) {
val nextIntent = Intent(requireContext(), AudioContentDetailActivity::class.java)
nextIntent.putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
startActivity(nextIntent)
}
} catch (_: IllegalStateException) {
}
}
private fun setupView() {
@ -144,17 +124,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
}
binding.swipeRefreshLayout.setOnRefreshListener { refreshSummary() }
val ivHowToUseLp = binding.ivHowToUse.layoutParams as LinearLayout.LayoutParams
ivHowToUseLp.width = screenWidth
ivHowToUseLp.height = (200 * screenWidth) / 1080
binding.ivHowToUse.layoutParams = ivHowToUseLp
binding.ivHowToUse.setOnClickListener {
val url = "https://blog.naver.com/yozmlive"
if (URLUtil.isValidUrl(url)) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
}
}
private fun refreshSummary() {
@ -186,7 +155,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
.layoutParams = layoutParams
binding.layoutRecommendLive.pager.apply {
adapter = RecommendLiveAdapter(pagerWidth.roundToInt(), pagerHeight) {
adapter = RecommendLiveAdapter(requireContext(), pagerWidth.roundToInt(), pagerHeight) {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
@ -373,11 +342,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
recyclerView.visibility = View.GONE
binding.layoutLiveNow.tvAllView.visibility = View.GONE
binding.layoutLiveNow.llNoItems.visibility = View.VISIBLE
binding.layoutLiveNow.tvMakeRoom.setOnClickListener {
val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
activityResultLauncher.launch(intent)
}
recyclerView.requestLayout()
binding.layoutLiveNow.llNoItems.requestLayout()
} else {
@ -460,12 +424,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
recyclerView.visibility = View.GONE
binding.layoutLiveReservation.tvAllView.visibility = View.GONE
binding.layoutLiveReservation.llNoItems.visibility = View.VISIBLE
binding.layoutLiveReservation.tvMakeRoom.setOnClickListener {
val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
intent.putExtra(Constants.EXTRA_LIVE_TIME_NOW, false)
activityResultLauncher.launch(intent)
}
recyclerView.requestLayout()
binding.layoutLiveReservation.llNoItems.requestLayout()
} else {
@ -496,7 +454,20 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
binding.eventBannerSlider.layoutParams = imageSliderLp
binding.eventBannerSlider.apply {
adapter = EventBannerAdapter(requireContext()) {} as BaseBannerAdapter<Any>
adapter = EventBannerAdapter(requireContext()) {
if (it.detailImageUrl != null) {
val intent = Intent(requireActivity(), EventDetailActivity::class.java)
intent.putExtra(Constants.EXTRA_EVENT, it)
startActivity(intent)
} else if (!it.link.isNullOrBlank()) {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(it.link)
)
)
}
} as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(800)
}.create()

View File

@ -25,10 +25,10 @@ class LiveNowAdapter(
fun bind(item: GetRoomListResponse) {
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
}
binding.tvManager.text = item.managerNickname
binding.tvManager.text = item.creatorNickname
binding.tvNumberOfMembers.text = "${item.numberOfParticipate}"
binding.ivLock.visibility = if (item.isPrivateRoom) {
View.VISIBLE
@ -44,12 +44,6 @@ class LiveNowAdapter(
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_10_643bc8)
}
binding.iv19.visibility = if (item.isAdult) {
View.VISIBLE
} else {
View.GONE
}
binding.root.setOnClickListener { onClick(item) }
}
}

View File

@ -32,7 +32,7 @@ class LiveNowAllAdapter(
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
}
binding.tvNickname.text = item.managerNickname
binding.tvNickname.text = item.creatorNickname
binding.tvTitle.text = item.title
binding.tvTotal.text = "/${item.numberOfPeople}"
binding.tvNumberOfParticipants.text = item.numberOfParticipate.toString()
@ -68,17 +68,11 @@ class LiveNowAllAdapter(
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
0,
0,
R.drawable.ic_coin_w,
R.drawable.ic_can,
0
)
}
binding.iv19.visibility = if (item.isAdult) {
View.VISIBLE
} else {
View.GONE
}
binding.root.setOnClickListener { onClick(item) }
}
}

View File

@ -1,15 +1,19 @@
package kr.co.vividnext.sodalive.live.recommend
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.widget.FrameLayout
import android.widget.ImageView
import coil.load
import coil.transform.RoundedCornersTransformation
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.bannerview.BaseViewHolder
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.extensions.dpToPx
class RecommendLiveAdapter(
private val context: Context,
private val itemWidth: Int,
private val itemHeight: Int,
private val onClick: (Long) -> Unit
@ -26,12 +30,20 @@ class RecommendLiveAdapter(
layoutParams.width = itemWidth
layoutParams.height = itemHeight
ivRecommendLive.load(data.imageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
ivRecommendLive.layoutParams = layoutParams
Glide
.with(context)
.asBitmap()
.load(data.imageUrl)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
ivRecommendLive.setImageBitmap(resource)
ivRecommendLive.layoutParams = layoutParams
}
override fun onLoadCleared(placeholder: Drawable?) {
}
})
ivRecommendLive.setOnClickListener { onClick(data.creatorId) }
}

View File

@ -54,7 +54,7 @@ class LiveRecommendChannelAdapter(
fun bind(item: GetRecommendChannelResponse) {
binding.ivRecommendChannel.load(item.profileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(30f.dpToPx()))
}
binding.tvRecommendChannelNickname.text = item.nickname

View File

@ -61,7 +61,7 @@ class LiveReservationAdapter(
}
private fun isMyLive(item: GetRoomListResponse) =
item.managerId == SharedPreferenceManager.userId && isMain
item.creatorId == SharedPreferenceManager.userId && isMain
@SuppressLint("NotifyDataSetChanged")
fun clear() {
@ -76,11 +76,11 @@ class LiveReservationAdapter(
fun bind(item: GetRoomListResponse) {
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
}
binding.tvDate.text = item.beginDateTime
binding.tvNickname.text = item.managerNickname
binding.tvNickname.text = item.creatorNickname
binding.tvTitle.text = item.title
binding.root.setOnClickListener { onClick(item) }
binding.ivLock.visibility = if (item.isPrivateRoom) {
@ -102,12 +102,6 @@ class LiveReservationAdapter(
"${item.price.moneyFormat()}"
}
}
binding.iv19.visibility = if (item.isAdult) {
View.VISIBLE
} else {
View.GONE
}
}
}
@ -122,11 +116,11 @@ class LiveReservationAdapter(
}
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4f.dpToPx()))
}
binding.tvDate.text = item.beginDateTime
binding.tvNickname.text = item.managerNickname
binding.tvNickname.text = item.creatorNickname
binding.tvTitle.text = item.title
binding.root.setOnClickListener { onClick(item) }
@ -135,12 +129,6 @@ class LiveReservationAdapter(
} else {
View.GONE
}
binding.iv19.visibility = if (item.isAdult) {
View.VISIBLE
} else {
View.GONE
}
}
}
}

View File

@ -13,5 +13,5 @@ data class MakeLiveReservationResponse(
@SerializedName("price") val price: String,
@SerializedName("haveCan") val haveCan: Int,
@SerializedName("useCan") val useCan: Int,
@SerializedName("remainingCoin") val remainingCoin: Int
@SerializedName("remainingCan") val remainingCan: Int
) : Parcelable

View File

@ -34,9 +34,9 @@ class LiveReservationCompleteActivity : BaseActivity<ActivityLiveReservationComp
binding.tvDate.text = response.beginDateString
binding.tvPrice.text = response.price
binding.tvHaveCoin.text = "${response.haveCan}"
binding.tvUseCoin.text = "${response.useCan}"
binding.tvRemainingCoin.text = "${response.remainingCoin}"
binding.tvHaveCan.text = "${response.haveCan}"
binding.tvUseCan.text = "${response.useCan}"
binding.tvRemainingCan.text = "${response.remainingCan}"
binding.tvGoHome.setOnClickListener {
val intent = Intent(applicationContext, MainActivity::class.java)

View File

@ -113,7 +113,7 @@ class LiveReservationCancelActivity : BaseActivity<ActivityLiveReservationCancel
startActivity(Intent(applicationContext, MainActivity::class.java))
}
binding.tvCheckCoinStatus.setOnClickListener {
binding.tvCheckCanStatus.setOnClickListener {
startActivity(Intent(applicationContext, CanStatusActivity::class.java))
}
@ -147,15 +147,15 @@ class LiveReservationCancelActivity : BaseActivity<ActivityLiveReservationCancel
binding.tvTitle.text = response.title
binding.ivProfile.load(response.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
}
if (response.price > 0) {
binding.tvCheckCoinStatus.visibility = View.VISIBLE
binding.tvPrice.text = "${response.price}코인"
binding.tvCheckCanStatus.visibility = View.VISIBLE
binding.tvPrice.text = "${response.price}"
} else {
binding.tvCheckCoinStatus.visibility = View.GONE
binding.tvCheckCanStatus.visibility = View.GONE
binding.tvPrice.text = "무료"
}
}

View File

@ -26,12 +26,12 @@ class LiveReservationStatusAdapter(
binding.tvTitle.text = item.title
binding.ivProfile.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
}
binding.tvPrice.text = if (item.price > 0) {
"${item.price}코인"
"${item.price}"
} else {
"무료"
}

View File

@ -106,7 +106,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
private var isMicrophoneMute = false
private var isSpeaker = false
private var isSpeakerFold = false
private var isAvailableDonation = false
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
@ -210,7 +209,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
)
},
onClickInviteSpeaker = { memberId ->
if (speakerListAdapter.itemCount <= 9) {
if (speakerListAdapter.itemCount <= 4) {
inviteSpeaker(memberId)
} else {
showToast("스피커 정원이 초과했습니다.")
@ -286,7 +285,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
)
},
onClickInviteSpeaker = {
if (speakerListAdapter.itemCount <= 9) {
if (speakerListAdapter.itemCount <= 4) {
inviteSpeaker(it)
} else {
showToast("스피커 정원이 초과했습니다.")
@ -559,21 +558,27 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
viewModel.roomInfoLiveData.observe(this) { response ->
binding.tvTitle.text = response.title
binding.ivCover.load(response.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
}
isAvailableDonation = response.isAvailableDonation
binding.flDonation.visibility = if (response.isAvailableDonation) {
binding.tv19.visibility = if (response.isAdult) {
View.VISIBLE
} else {
View.GONE
}
binding.tvTitle.text = response.title
binding.ivCover.load(response.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
}
binding.flDonation.visibility =
if (response.creatorId != SharedPreferenceManager.userId) {
View.VISIBLE
} else {
View.GONE
}
if (
response.managerId == SharedPreferenceManager.userId &&
response.creatorId == SharedPreferenceManager.userId &&
SharedPreferenceManager.role == MemberRole.CREATOR.name
) {
binding.flDonationMessageList.visibility = View.VISIBLE
@ -605,10 +610,10 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
binding.flDonationMessageList.visibility = View.GONE
}
speakerListAdapter.managerId = response.managerId
speakerListAdapter.managerId = response.creatorId
speakerListAdapter.updateList(response.speakerList)
if (response.managerId == SharedPreferenceManager.userId) {
if (response.creatorId == SharedPreferenceManager.userId) {
binding.ivEdit.setOnClickListener {
roomInfoEditDialog.setRoomInfo(response.title, response.notice)
roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl)
@ -625,7 +630,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
if (newCoverImageUri != null) {
binding.ivCover.load(newCoverImageUri) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
}
}
@ -676,27 +681,26 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
binding.tvParticipate.text = "${response.participantsCount}"
setNoticeAndClickableUrl(binding.tvNotice, response.notice)
binding.tvCreatorNickname.text = response.managerNickname
binding.ivCreatorProfile.load(response.managerProfileUrl) {
binding.tvCreatorNickname.text = response.creatorNickname
binding.ivCreatorProfile.load(response.creatorProfileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.ivCreatorProfile.setOnClickListener {
if (response.managerId != SharedPreferenceManager.userId) {
showLiveRoomUserProfileDialog(userId = response.managerId)
if (response.creatorId != SharedPreferenceManager.userId) {
showLiveRoomUserProfileDialog(userId = response.creatorId)
}
}
if (response.isAvailableDonation) {
if (response.creatorId != SharedPreferenceManager.userId) {
binding.ivCreatorFollow.visibility = View.VISIBLE
if (response.isFollowingManager) {
if (response.isFollowing) {
binding.ivCreatorFollow.setImageResource(R.drawable.btn_following)
binding.ivCreatorFollow.setOnClickListener {
viewModel.creatorUnFollow(
creatorId = response.managerId,
creatorId = response.creatorId,
roomId = roomId
)
}
@ -704,7 +708,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
binding.ivCreatorFollow.setImageResource(R.drawable.btn_follow)
binding.ivCreatorFollow.setOnClickListener {
viewModel.creatorFollow(
creatorId = response.managerId,
creatorId = response.creatorId,
roomId = roomId
)
}
@ -1190,7 +1194,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
},
rtmChannelJoinSuccess = {
if (userId == roomInfo.managerId) {
if (userId == roomInfo.creatorId) {
setBroadcaster()
} else {
setAudience()

View File

@ -118,7 +118,7 @@ class LiveRoomViewModel(
}
fun getManagerNickname(): String {
return roomInfoResponse.managerNickname
return roomInfoResponse.creatorNickname
}
fun setSpeaker(roomId: Long, userId: Long, onSuccess: () -> Unit) {
@ -190,7 +190,7 @@ class LiveRoomViewModel(
getTotalDonationCan(roomId = roomId)
if (userId > 0) {
if (userId > 0 && it.data.creatorId == SharedPreferenceManager.userId) {
val nickname = getUserNickname(userId)
onSuccess(nickname)
}
@ -213,7 +213,7 @@ class LiveRoomViewModel(
}
fun isEqualToHostId(memberId: Int): Boolean {
return memberId == roomInfoResponse.managerId.toInt()
return memberId == roomInfoResponse.creatorId.toInt()
}
fun getMemberCan() {
@ -241,11 +241,11 @@ class LiveRoomViewModel(
) {
_isLoading.value = true
Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) {
link = Uri.parse("https://yozm.day/?room_id=$roomId")
domainUriPrefix = "https://yozm.page.link"
link = Uri.parse("https://sodalive.net/?room_id=$roomId")
domainUriPrefix = "https://sodalive.page.link"
androidParameters { }
iosParameters("kr.co.vividnext.yozm") {
appStoreId = "1630284226"
iosParameters("kr.co.vividnext.sodalive") {
appStoreId = "6461721697"
}
}.addOnSuccessListener {
val uri = it.shortLink

View File

@ -160,7 +160,7 @@ data class LiveRoomNormalChat(
val itemBinding = binding as ItemLiveRoomChatBinding
itemBinding.ivProfile.load(profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(23.3f.dpToPx()))
}
@ -233,7 +233,7 @@ data class LiveRoomNormalChat(
} else {
itemBinding.llMessageBg.setBackgroundResource(R.drawable.bg_round_corner_3_3_99000000)
}
itemBinding.ivCoin.visibility = View.GONE
itemBinding.ivCan.visibility = View.GONE
itemBinding.tvDonationMessage.visibility = View.GONE
itemBinding.root.setBackgroundResource(0)
itemBinding.root.setPadding(0)
@ -244,7 +244,7 @@ data class LiveRoomDonationChat(
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("nickname") val nickname: String,
@SerializedName("chat") val chat: String,
@SerializedName("coin") val coin: Int,
@SerializedName("can") val can: Int,
@SerializedName("donationMessage") val donationMessage: String,
) : LiveRoomChat() {
@SuppressLint("SetTextI18n")
@ -278,14 +278,14 @@ data class LiveRoomDonationChat(
itemBinding.ivProfile.load(profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(23.3f.dpToPx()))
}
itemBinding.tvChat.text = spChat
itemBinding.tvNickname.text = spNickname
itemBinding.ivProfile.setOnClickListener {}
itemBinding.ivCoin.visibility = View.VISIBLE
itemBinding.ivCan.visibility = View.VISIBLE
itemBinding.ivBg.visibility = View.GONE
itemBinding.ivCrown.visibility = View.GONE
itemBinding.tvCreatorOrManager.visibility = View.GONE
@ -302,27 +302,27 @@ data class LiveRoomDonationChat(
itemBinding.root.setBackgroundResource(
when {
coin >= 100000 -> {
can >= 100000 -> {
R.drawable.bg_round_corner_6_7_c25264
}
coin >= 50000 -> {
can >= 50000 -> {
R.drawable.bg_round_corner_6_7_e6d85e37
}
coin >= 10000 -> {
can >= 10000 -> {
R.drawable.bg_round_corner_6_7_e6d38c38
}
coin >= 5000 -> {
can >= 5000 -> {
R.drawable.bg_round_corner_6_7_e659548f
}
coin >= 1000 -> {
can >= 1000 -> {
R.drawable.bg_round_corner_6_7_e64d6aa4
}
coin >= 500 -> {
can >= 500 -> {
R.drawable.bg_round_corner_6_7_e62d7390
}

View File

@ -105,7 +105,7 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
binding.ivCover.background = null
binding.ivCover.load(fileUri) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
viewModel.coverImageUri = fileUri
@ -282,7 +282,7 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
binding.ivCover.background = null
binding.ivCover.load(it.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
}

View File

@ -11,6 +11,7 @@ data class GetRoomDetailResponse(
@SerializedName("title") val title: String,
@SerializedName("notice") val notice: String,
@SerializedName("isPaid") val isPaid: Boolean,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
@SerializedName("password") val password: Int?,
@SerializedName("tags") val tags: List<String>,

View File

@ -1,50 +0,0 @@
package kr.co.vividnext.sodalive.live.room.detail
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDetailUserBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class LiveRoomDetailAdapter(
private val onClick: (GetRoomDetailUser) -> Unit
) : RecyclerView.Adapter<LiveRoomDetailAdapter.ViewHolder>() {
val items = mutableListOf<GetRoomDetailUser>()
inner class ViewHolder(
private val binding: ItemLiveRoomDetailUserBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetRoomDetailUser) {
binding.tvNickname.text = item.nickname
binding.ivProfile.load(item.profileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(RoundedCornersTransformation(23.4f.dpToPx()))
}
binding.root.setOnClickListener { onClick(item) }
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
return ViewHolder(
ItemLiveRoomDetailUserBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.count()
}

View File

@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.live.room.detail
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
@ -10,21 +9,17 @@ import android.view.View
import android.view.ViewGroup
import android.webkit.URLUtil
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentLiveRoomDetailBinding
import kr.co.vividnext.sodalive.databinding.ItemLiveDetailUserSummaryBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import org.koin.android.ext.android.inject
class LiveRoomDetailFragment(
@ -40,9 +35,6 @@ class LiveRoomDetailFragment(
private lateinit var binding: FragmentLiveRoomDetailBinding
private var isAllProfileOpen = false
private lateinit var adapter: LiveRoomDetailAdapter
private lateinit var loadingDialog: LoadingDialog
private lateinit var roomDetail: GetRoomDetailResponse
@ -65,58 +57,10 @@ class LiveRoomDetailFragment(
val behavior = BottomSheetBehavior.from<View>(bottomSheet!!)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
setupAdapter()
bindData()
viewModel.getDetail(roomId) { dismiss() }
binding.ivClose.setOnClickListener { dismiss() }
binding.tvOpenAllProfile.setOnClickListener {
isAllProfileOpen = !isAllProfileOpen
if (isAllProfileOpen) {
binding.llProfiles.visibility = View.GONE
binding.rvParticipate.visibility = View.VISIBLE
binding.tvParticipateExpression.visibility = View.VISIBLE
binding.tvOpenAllProfile.text = "닫기"
binding.tvOpenAllProfile.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_live_detail_top,
0,
0,
0
)
} else {
binding.llProfiles.visibility = View.VISIBLE
binding.rvParticipate.visibility = View.GONE
binding.tvParticipateExpression.visibility = View.GONE
binding.tvOpenAllProfile.text = "펼쳐보기"
binding.tvOpenAllProfile.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_live_detail_bottom,
0,
0,
0
)
}
}
}
private fun setupAdapter() {
val recyclerView = binding.rvParticipate
adapter = LiveRoomDetailAdapter {}
recyclerView.layoutManager = GridLayoutManager(requireContext(), 5)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
})
recyclerView.adapter = adapter
}
private fun bindData() {
@ -140,28 +84,26 @@ class LiveRoomDetailFragment(
@SuppressLint("SetTextI18n", "NotifyDataSetChanged")
private fun setRoomDetail(response: GetRoomDetailResponse) {
binding.tvTitle.text = response.title
binding.tvDate.text = response.beginDateTime
binding.tvParticipate.text = response.numberOfParticipants.toString()
binding.tvTotal.text = "/${response.numberOfParticipantsTotal}"
binding.tvOpenAllProfile.visibility = if (response.numberOfParticipants <= 0) {
View.GONE
} else {
binding.tv19.visibility = if (response.isAdult) {
View.VISIBLE
} else {
View.GONE
}
binding.tvTitle.text = response.title
binding.tvDate.text = response.beginDateTime
if (response.price > 0) {
binding.tvCoin.text = response.price.toString()
binding.tvCoin.setCompoundDrawablesWithIntrinsicBounds(
binding.tvCan.text = response.price.toString()
binding.tvCan.setCompoundDrawablesWithIntrinsicBounds(
0,
0,
R.drawable.ic_can,
0
)
} else {
binding.tvCoin.text = "무료"
binding.tvCoin.setCompoundDrawablesWithIntrinsicBounds(
binding.tvCan.text = "무료"
binding.tvCan.setCompoundDrawablesWithIntrinsicBounds(
0,
0,
0,
@ -170,13 +112,14 @@ class LiveRoomDetailFragment(
}
setManagerProfile(manager = response.manager)
setParticipantUserSummary(response.participatingUsers)
binding.tvTags.text = response.tags.joinToString(" ") { "#$it" }
binding.tvContent.text = response.notice
binding.ivShare.setOnClickListener { shareRoom(response) }
binding.ivShare2.setOnClickListener { shareRoom(response) }
if (response.channelName.isNullOrBlank()) {
binding.tvParticipateExpression.text = "예약자"
when {
response.manager.id == SharedPreferenceManager.userId -> {
binding.llStartDelete.visibility = View.VISIBLE
@ -216,7 +159,6 @@ class LiveRoomDetailFragment(
}
}
} else {
binding.tvParticipateExpression.text = "참여자"
binding.tvReservationComplete.visibility = View.GONE
binding.tvParticipateNow.visibility = View.VISIBLE
binding.tvReservation.visibility = View.GONE
@ -226,9 +168,6 @@ class LiveRoomDetailFragment(
}
binding.llStartDelete.visibility = View.GONE
}
adapter.items.addAll(response.participatingUsers)
adapter.notifyDataSetChanged()
}
private fun setManagerProfile(manager: GetRoomDetailManager) {
@ -236,7 +175,7 @@ class LiveRoomDetailFragment(
binding.tvManagerIntroduce.text = manager.introduce
binding.ivManagerProfile.load(manager.profileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
@ -290,35 +229,28 @@ class LiveRoomDetailFragment(
if (manager.isCreator) {
binding.tvManagerProfile.visibility = View.VISIBLE
binding.tvManagerProfile.setOnClickListener {}
binding.tvManagerProfile.setOnClickListener {
val intent = Intent(requireActivity(), UserProfileActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, manager.id)
startActivity(intent)
}
} else {
binding.tvManagerProfile.visibility = View.GONE
}
}
private fun setParticipantUserSummary(participatingUsers: List<GetRoomDetailUser>) {
val userCount = if (participatingUsers.size > 10) {
10
} else {
participatingUsers.size
}
private fun shareRoom(response: GetRoomDetailResponse) {
viewModel.shareRoomLink(
response.roomId,
response.isPrivateRoom,
response.password
) {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, it)
for (index in 0 until userCount) {
val user = participatingUsers[index]
val itemView = ItemLiveDetailUserSummaryBinding.inflate(layoutInflater)
itemView.ivProfile.load(user.profileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(RoundedCornersTransformation(16.7f.dpToPx()))
}
val lp = LinearLayout.LayoutParams(33.3f.dpToPx().toInt(), 33.3f.dpToPx().toInt())
if (index > 0) {
lp.setMargins(-16.7f.dpToPx().toInt(), 0, 0, 0)
}
itemView.root.layoutParams = lp
binding.llProfiles.addView(itemView.root)
val shareIntent = Intent.createChooser(intent, "라이브 공유")
startActivity(shareIntent)
}
}
}

View File

@ -63,4 +63,41 @@ class LiveRoomDetailViewModel(private val repository: LiveRepository) : BaseView
)
)
}
fun shareRoomLink(
roomId: Long,
isPrivateRoom: Boolean,
password: Int?,
onSuccess: (String) -> Unit
) {
_isLoading.value = true
Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) {
link = Uri.parse("https://sodalive.net/?room_id=$roomId")
domainUriPrefix = "https://sodalive.page.link"
androidParameters { }
iosParameters("kr.co.vividnext.sodalive") {
appStoreId = "6461721697"
}
}.addOnSuccessListener {
val uri = it.shortLink
if (uri != null) {
val message = if (isPrivateRoom) {
"${SharedPreferenceManager.nickname}님이 귀하를 " +
"소다라이브의 비공개라이브에 초대하였습니다.\n" +
"※ 라이브 참여: $uri\n" +
"(입장 비밀번호 : $password)"
} else {
"${SharedPreferenceManager.nickname}님이 귀하를 " +
"소다라이브의 공개라이브에 초대하였습니다.\n" +
"※ 라이브 참여: $uri"
}
onSuccess(message)
}
}.addOnFailureListener {
_toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.")
}.addOnCompleteListener {
_isLoading.value = false
}
}
}

View File

@ -36,11 +36,11 @@ class LiveRoomPasswordDialog(
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
if (can > 0) {
dialogView.tvCoin.visibility = View.VISIBLE
dialogView.tvCoin.text = can.moneyFormat()
dialogView.tvCan.visibility = View.VISIBLE
dialogView.tvCan.text = can.moneyFormat()
dialogView.tvConfirm.text = "으로 입장"
} else {
dialogView.tvCoin.visibility = View.GONE
dialogView.tvCan.visibility = View.GONE
dialogView.tvConfirm.text = "입장하기"
}

View File

@ -5,11 +5,14 @@ import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import coil.load
import coil.transform.CircleCropTransformation
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.Constants
@ -32,26 +35,36 @@ class LiveRoomDonationDialog(
bottomSheetDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
bottomSheetDialog.setCancelable(false)
val bottomSheetInternal = bottomSheetDialog.findViewById<FrameLayout>(
com.google.android.material.R.id.design_bottom_sheet
)
if (bottomSheetInternal != null) {
val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetInternal)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
bottomSheetBehavior.skipCollapsed = true
}
dialogView.tvCancel.setOnClickListener { bottomSheetDialog.dismiss() }
dialogView.tvDonation.setOnClickListener {
try {
val coin = dialogView.etDonationCoin.text.toString().toInt()
val can = dialogView.etDonationCan.text.toString().toInt()
val message = dialogView.etDonationMessage.text.toString()
if (coin > 0) {
if (can > 0) {
bottomSheetDialog.dismiss()
onClickDonation(coin, message)
onClickDonation(can, message)
} else {
Toast.makeText(
activity,
"1코인 이상 후원하실 수 있습니다.",
"1 이상 후원하실 수 있습니다.",
Toast.LENGTH_LONG
).show()
}
} catch (e: NumberFormatException) {
Toast.makeText(
activity,
"1코인 이상 후원하실 수 있습니다.",
"1 이상 후원하실 수 있습니다.",
Toast.LENGTH_LONG
).show()
}
@ -70,24 +83,28 @@ class LiveRoomDonationDialog(
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
bottomSheetDialog.window?.attributes = lp
dialogView.scrollView.post {
dialogView.scrollView.fullScroll(View.FOCUS_DOWN)
}
}
}
@SuppressLint("SetTextI18n")
private fun setupView() {
dialogView.tvCoin.text = SharedPreferenceManager.can.moneyFormat()
dialogView.tvPlus10.setOnClickListener { addCoin(10) }
dialogView.tvPlus100.setOnClickListener { addCoin(100) }
dialogView.tvPlus1000.setOnClickListener { addCoin(1000) }
dialogView.tvPlus10000.setOnClickListener { addCoin(10000) }
dialogView.tvCan.text = SharedPreferenceManager.can.moneyFormat()
dialogView.tvPlus10.setOnClickListener { addCan(10) }
dialogView.tvPlus100.setOnClickListener { addCan(100) }
dialogView.tvPlus1000.setOnClickListener { addCan(1000) }
dialogView.tvPlus10000.setOnClickListener { addCan(10000) }
dialogView.ivProfile.load(SharedPreferenceManager.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
dialogView.tvCoin.setOnClickListener {
dialogView.tvCan.setOnClickListener {
bottomSheetDialog.dismiss()
val intent = Intent(activity, CanChargeActivity::class.java)
@ -97,12 +114,12 @@ class LiveRoomDonationDialog(
}
@SuppressLint("SetTextI18n")
private fun addCoin(coin: Int) {
private fun addCan(can: Int) {
try {
val currentCoin = dialogView.etDonationCoin.text.toString().toInt()
dialogView.etDonationCoin.setText((currentCoin + coin).toString())
val currentCan = dialogView.etDonationCan.text.toString().toInt()
dialogView.etDonationCan.setText((currentCan + can).toString())
} catch (e: NumberFormatException) {
dialogView.etDonationCoin.setText(coin.toString())
dialogView.etDonationCan.setText(can.toString())
}
}
}

View File

@ -21,7 +21,7 @@ class LiveRoomDonationMessageAdapter(
@SuppressLint("SetTextI18n")
fun bind(item: LiveRoomDonationMessage) {
binding.tvNickname.text = "${item.nickname}님이"
binding.tvCoinMessage.text = item.canMessage
binding.tvCanMessage.text = item.canMessage
binding.tvDonationMessage.text = "\"${item.donationMessage}\""
binding.ivDelete.setOnClickListener { onClickDeleteMessage(item.uuid) }

View File

@ -45,7 +45,7 @@ class LiveRoomDonationRankingAdapter :
binding.ivProfile.load(item.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}

View File

@ -35,7 +35,7 @@ class LiveRoomDonationRankingDialog(
adapter.items.clear()
adapter.items.addAll(it.donationList)
adapter.notifyDataSetChanged()
dialogView.tvTotalCoin.text = it.totalCan.moneyFormat()
dialogView.tvTotalCan.text = it.totalCan.moneyFormat()
dialogView.tvTotalCount.text = it.totalCount.moneyFormat()
}
}

View File

@ -10,17 +10,17 @@ data class GetRoomInfoResponse(
@SerializedName("channelName") val channelName: String,
@SerializedName("rtcToken") val rtcToken: String,
@SerializedName("rtmToken") val rtmToken: String,
@SerializedName("managerId") val managerId: Long,
@SerializedName("managerNickname") val managerNickname: String,
@SerializedName("managerProfileUrl") val managerProfileUrl: String,
@SerializedName("isFollowingManager") val isFollowingManager: Boolean,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("creatorProfileUrl") val creatorProfileUrl: String,
@SerializedName("isFollowing") val isFollowing: Boolean,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("participantsCount") val participantsCount: Int,
@SerializedName("totalAvailableParticipantsCount") val totalAvailableParticipantsCount: Int,
@SerializedName("speakerList") val speakerList: List<LiveRoomMember>,
@SerializedName("listenerList") val listenerList: List<LiveRoomMember>,
@SerializedName("managerList") val managerList: List<LiveRoomMember>,
@SerializedName("donationRankingTop3UserIds") val donationRankingTop3UserIds: List<Long>,
@SerializedName("isAvailableDonation") val isAvailableDonation: Boolean = false,
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
@SerializedName("password") val password: String? = null
)

View File

@ -52,7 +52,7 @@ class LiveRoomProfileDialog(
@SuppressLint("SetTextI18n")
private fun bindData() {
roomInfoLiveData.observe(activity) {
adapter.managerId = it.managerId
adapter.managerId = it.creatorId
adapter.totalUserCount = it.totalAvailableParticipantsCount
dialogView.tvParticipate.text = "${it.participantsCount}"
dialogView.tvTotalPeoples.text = "/${it.totalAvailableParticipantsCount}"

View File

@ -57,8 +57,8 @@ data class LiveRoomProfileItemSpeakerTitle(
val itemBinding = binding as ItemLiveRoomProfileHeaderBinding
itemBinding.tvTitle.text = title
itemBinding.tvSpeakerCount.text = "$speakerCount"
itemBinding.tvSpeakerTotalCount.text = if (totalUserCount > 9) {
"/9"
itemBinding.tvSpeakerTotalCount.text = if (totalUserCount > 4) {
"/4"
} else {
"/${totalUserCount - 1}"
}
@ -118,7 +118,7 @@ data class LiveRoomProfileItemMaster(
itemBinding.tvNickname.text = nickname
itemBinding.ivProfile.load(profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
if (id != SharedPreferenceManager.userId) {
@ -137,7 +137,7 @@ data class LiveRoomProfileItemManager(
itemBinding.tvNickname.text = nickname
itemBinding.ivProfile.load(profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
if (id != SharedPreferenceManager.userId) {
@ -159,7 +159,7 @@ data class LiveRoomProfileItemUser(
itemBinding.tvNickname.text = nickname
itemBinding.ivProfile.load(profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}

View File

@ -19,7 +19,7 @@ class LiveRoomProfileListAdapter : RecyclerView.Adapter<LiveRoomProfileListAdapt
fun bind(item: LiveRoomMember) {
binding.ivProfile.load(item.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(23.3f.dpToPx()))
if (activeSpeakers.contains(item.id.toInt())) {

View File

@ -78,7 +78,7 @@ class LiveRoomUserProfileDialog(
userProfileLiveData.observe(activity) { userProfile ->
dialogView.ivProfile.load(userProfile.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(8.dpToPx()))
}

View File

@ -36,7 +36,7 @@ class LiveTagAdapter(
binding.ivTag.load(item.image) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(30f.dpToPx()))
}
binding.tvTag.text = item.tag

View File

@ -4,7 +4,7 @@ import com.google.gson.annotations.SerializedName
data class EditLiveRoomInfoRequest(
@SerializedName("title") val title: String?,
@SerializedName("content") val notice: String?,
@SerializedName("notice") val notice: String?,
@SerializedName("numberOfPeople") val numberOfPeople: Int?,
@SerializedName("beginDateTimeString") val beginDateTimeString: String?,
@SerializedName("timezone") val timezone: String?

View File

@ -49,7 +49,7 @@ class LiveRoomInfoEditDialog(
this.coverImageUri = coverImageUri
dialogView.ivCover.load(coverImageUri) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
}
@ -58,7 +58,7 @@ class LiveRoomInfoEditDialog(
this.coverImageUrl = coverImageUrl
dialogView.ivCover.load(coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
}

View File

@ -52,40 +52,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val bundle = intent.getBundleExtra(Constants.EXTRA_DATA)
if (bundle != null) {
try {
val roomId = bundle.getLong(Constants.EXTRA_ROOM_ID)
val channelId = bundle.getLong(Constants.EXTRA_USER_ID)
val audioContentId = bundle.getLong(Constants.EXTRA_AUDIO_CONTENT_ID)
val isLiveReservation = bundle.getBoolean(Constants.EXTRA_LIVE_RESERVATION_RESPONSE)
if (roomId > 0) {
if (isLiveReservation) {
liveFragment.reservationRoom(roomId)
} else {
handler.postDelayed({
liveFragment.enterLiveRoom(roomId)
}, 500)
}
} else if (channelId > 0) {
val nextIntent = Intent(applicationContext, UserProfileActivity::class.java)
nextIntent.putExtra(Constants.EXTRA_USER_ID, channelId)
startActivity(nextIntent)
} else if (audioContentId > 0) {
val nextIntent = Intent(
applicationContext,
AudioContentDetailActivity::class.java
)
nextIntent.putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
startActivity(nextIntent)
}
} catch (_: IllegalStateException) {
}
}
checkReceivedMessage(intent)
executeDeeplink()
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -95,7 +62,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
getMemberInfo()
getEventPopup()
checkReceivedMessage(intent)
handler.postDelayed({ executeDeeplink() }, 500)
}
override fun onResume() {
@ -115,25 +83,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
unregisterReceiver(audioContentReceiver)
}
private fun checkReceivedMessage(intent: Intent) {
handler.postDelayed({
val messageId =
intent.getBundleExtra(Constants.EXTRA_DATA)?.getLong(Constants.EXTRA_MESSAGE_ID)
if (messageId != null && messageId > 0) {
changeFragment(MainViewModel.CurrentTab.MESSAGE)
setTabSelected(binding.tabSuda, isSelected = false)
setTabSelected(binding.tabExplorer, isSelected = false)
setTabSelected(binding.tabMessage, isSelected = true)
setTabSelected(binding.tabMy, isSelected = false)
}
}, 500)
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
liveFragment = LiveFragment().apply {
arguments = intent.getBundleExtra(Constants.EXTRA_DATA)
}
liveFragment = LiveFragment()
notificationSettingsDialog = NotificationSettingsDialog(
this,
@ -149,6 +101,57 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
setupBottomTabLayout()
}
private fun executeDeeplink() {
val bundle = intent.getBundleExtra(Constants.EXTRA_DATA)
if (bundle != null) {
try {
val roomId = bundle.getLong(Constants.EXTRA_ROOM_ID)
val channelId = bundle.getLong(Constants.EXTRA_USER_ID)
val messageId = bundle.getLong(Constants.EXTRA_MESSAGE_ID)
val contentId = bundle.getLong(Constants.EXTRA_AUDIO_CONTENT_ID)
val isLiveReservation = bundle.getBoolean(Constants.EXTRA_LIVE_RESERVATION_RESPONSE)
if (roomId > 0) {
changeFragment(MainViewModel.CurrentTab.LIVE)
setTabSelected(binding.tabLive, isSelected = true)
setTabSelected(binding.tabExplorer, isSelected = false)
setTabSelected(binding.tabMessage, isSelected = false)
setTabSelected(binding.tabMy, isSelected = false)
setTabSelected(binding.tabContent, isSelected = false)
handler.postDelayed({
if (isLiveReservation) {
liveFragment.reservationRoom(roomId)
} else {
liveFragment.enterLiveRoom(roomId)
}
}, 500)
} else if (channelId > 0) {
val nextIntent = Intent(applicationContext, UserProfileActivity::class.java)
nextIntent.putExtra(Constants.EXTRA_USER_ID, channelId)
startActivity(nextIntent)
} else if (contentId > 0) {
val nextIntent = Intent(
applicationContext,
AudioContentDetailActivity::class.java
)
nextIntent.putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
startActivity(nextIntent)
} else if (messageId > 0) {
handler.postDelayed({
changeFragment(MainViewModel.CurrentTab.MESSAGE)
setTabSelected(binding.tabLive, isSelected = false)
setTabSelected(binding.tabExplorer, isSelected = false)
setTabSelected(binding.tabMessage, isSelected = true)
setTabSelected(binding.tabMy, isSelected = false)
setTabSelected(binding.tabContent, isSelected = false)
}, 500)
}
} catch (_: IllegalStateException) {
}
}
}
private fun setupBottomTabLayout() {
setupTab(
binding = binding.tabContent,
@ -162,7 +165,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
)
setupTab(
binding = binding.tabSuda,
binding = binding.tabLive,
title = "라이브",
imageSrc = R.drawable.ic_tabbar_live,
colorStateList = ContextCompat.getColorStateList(
@ -207,7 +210,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
viewModel.currentTab.observe(this) {
setTabSelected(binding.tabContent, isSelected = false)
setTabSelected(binding.tabSuda, isSelected = false)
setTabSelected(binding.tabLive, isSelected = false)
setTabSelected(binding.tabExplorer, isSelected = false)
setTabSelected(binding.tabMessage, isSelected = false)
setTabSelected(binding.tabMy, isSelected = false)
@ -219,7 +222,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
}
MainViewModel.CurrentTab.LIVE -> {
setTabSelected(binding.tabSuda, isSelected = true)
setTabSelected(binding.tabLive, isSelected = true)
}
MainViewModel.CurrentTab.EXPLORER -> {

View File

@ -39,7 +39,7 @@ class MainViewModel(
MY
}
private val _currentTab = MutableLiveData(CurrentTab.LIVE)
private val _currentTab = MutableLiveData(CurrentTab.CONTENT)
val currentTab: LiveData<CurrentTab>
get() = _currentTab

View File

@ -20,7 +20,7 @@ class SelectMessageRecipientAdapter(
fun bind(item: GetRoomDetailUser) {
binding.ivProfile.load(item.profileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(23.4f.dpToPx()))
}

View File

@ -23,7 +23,7 @@ class TextMessageAdapter(
if (SharedPreferenceManager.nickname == item.recipientNickname) {
binding.ivProfile.load(item.senderProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(23.4f.dpToPx()))
}
@ -31,7 +31,7 @@ class TextMessageAdapter(
} else {
binding.ivProfile.load(item.recipientProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(23.4f.dpToPx()))
}

View File

@ -114,7 +114,7 @@ class TextMessageDetailActivity : BaseActivity<ActivityTextMessageDetailBinding>
if (SharedPreferenceManager.nickname == messageItem?.recipientNickname) {
binding.ivProfile.load(messageItem?.senderProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
@ -122,7 +122,7 @@ class TextMessageDetailActivity : BaseActivity<ActivityTextMessageDetailBinding>
} else {
binding.ivProfile.load(messageItem?.recipientProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}

View File

@ -57,7 +57,7 @@ class VoiceMessageAdapter(
if (SharedPreferenceManager.nickname == item.recipientNickname) {
binding.ivProfile.load(item.senderProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(23.4f.dpToPx()))
}
@ -65,7 +65,7 @@ class VoiceMessageAdapter(
} else {
binding.ivProfile.load(item.recipientProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(23.4f.dpToPx()))
}

View File

@ -103,7 +103,7 @@ class VoiceMessageWriteFragment(
private fun setReceiver(userId: Long, nickname: String, profileUrl: String) {
binding.ivProfile.load(profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(23.4f.dpToPx()))
}
binding.tvNickname.text = nickname
@ -180,7 +180,7 @@ class VoiceMessageWriteFragment(
}
fileNameMedia =
"${requireActivity().filesDir.path}/socdoc_${System.currentTimeMillis()}.mp3"
"${requireActivity().filesDir.path}/voice_message_${System.currentTimeMillis()}.mp3"
val fileMedia = File(fileNameMedia)
if (!fileMedia.exists()) {

View File

@ -2,28 +2,37 @@ package kr.co.vividnext.sodalive.mypage
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.webkit.URLUtil
import android.widget.LinearLayout
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import com.google.gson.Gson
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListAdapter
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentMyBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.reservation_status.LiveReservationStatusActivity
import kr.co.vividnext.sodalive.mypage.auth.Auth
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity
import kr.co.vividnext.sodalive.live.reservation_status.LiveReservationStatusActivity
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateActivity
import kr.co.vividnext.sodalive.mypage.service_center.ServiceCenterActivity
import kr.co.vividnext.sodalive.settings.SettingsActivity
import kr.co.vividnext.sodalive.settings.notification.MemberRole
@ -34,6 +43,7 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
private val viewModel: MyPageViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var orderListAdapter: AudioContentOrderListAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -49,6 +59,42 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
}
private fun setupView() {
orderListAdapter = AudioContentOrderListAdapter {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java)
.apply { putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) }
)
}
binding.rvOrderList.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.VERTICAL,
false
)
binding.rvOrderList.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
orderListAdapter.itemCount - 1 -> {
outRect.bottom = 0
}
else -> {
outRect.bottom = 13.3f.dpToPx().toInt()
}
}
}
})
binding.rvOrderList.adapter = orderListAdapter
binding.ivSettings.setOnClickListener {
startActivity(
Intent(
@ -58,7 +104,14 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
)
}
binding.ivEdit.setOnClickListener {}
binding.ivEdit.setOnClickListener {
startActivity(
Intent(
requireActivity(),
ProfileUpdateActivity::class.java
)
)
}
binding.llTotalCan.setOnClickListener {
startActivity(
@ -124,9 +177,20 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
} else {
binding.tvMyChannel.visibility = View.GONE
}
val ivHowToUseLp = binding.ivHowToUse.layoutParams as LinearLayout.LayoutParams
ivHowToUseLp.width = screenWidth
ivHowToUseLp.height = (200 * screenWidth) / 1080
binding.ivHowToUse.layoutParams = ivHowToUseLp
binding.ivHowToUse.setOnClickListener {
val url = "https://blog.naver.com/sodalive_official"
if (URLUtil.isValidUrl(url)) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
}
}
@SuppressLint("SetTextI18n")
@SuppressLint("SetTextI18n", "NotifyDataSetChanged")
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
@ -149,7 +213,7 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
binding.ivProfile.load(it.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvNickname.text = it.nickname
@ -191,7 +255,24 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
}
binding.tvTotalCan.text = (it.chargeCan + it.rewardCan).moneyFormat()
binding.tvReservationSuda.text = "${it.liveReservationCount}"
binding.tvReservationLive.text = "${it.liveReservationCount}"
if (it.orderList.totalCount > 0) {
binding.llOrderList.visibility = View.VISIBLE
orderListAdapter.items.addAll(it.orderList.items)
orderListAdapter.notifyDataSetChanged()
binding.tvAllOrderList.setOnClickListener {
startActivity(
Intent(
requireContext(),
AudioContentOrderListActivity::class.java
)
)
}
} else {
binding.llOrderList.visibility = View.GONE
}
}
}
}

View File

@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.mypage
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListResponse
data class MyPageResponse(
@SerializedName("nickname") val nickname: String,
@ -14,4 +15,5 @@ data class MyPageResponse(
@SerializedName("liveReservationCount") val liveReservationCount: Int,
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("isAuth") val isAuth: Boolean,
@SerializedName("orderList") val orderList: GetAudioContentOrderListResponse
)

View File

@ -207,7 +207,7 @@ class CanPaymentActivity : BaseActivity<ActivityCanPaymentBinding>(
viewModel.verify(
request,
onSuccess = {
Toast.makeText(applicationContext, "코인이 충전되었습니다", Toast.LENGTH_LONG).show()
Toast.makeText(applicationContext, "이 충전되었습니다", Toast.LENGTH_LONG).show()
if (prevLiveRoom) {
setResult(RESULT_OK)
} else {

View File

@ -0,0 +1,20 @@
package kr.co.vividnext.sodalive.mypage.profile
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.user.Gender
data class ProfileResponse(
@SerializedName("userId") val userId: Long,
@SerializedName("email") val email: String,
@SerializedName("nickname") val nickname: String,
@SerializedName("gender") val gender: Gender,
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("chargeCan") val chargeCan: Int,
@SerializedName("rewardCan") val rewardCan: Int,
@SerializedName("youtubeUrl") val youtubeUrl: String?,
@SerializedName("instagramUrl") val instagramUrl: String?,
@SerializedName("blogUrl") val blogUrl: String?,
@SerializedName("websiteUrl") val websiteUrl: String?,
@SerializedName("introduce") val introduce: String,
@SerializedName("tags") val tags: List<String>
)

View File

@ -0,0 +1,277 @@
package kr.co.vividnext.sodalive.mypage.profile
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import coil.load
import coil.transform.CircleCropTransformation
import com.github.dhaval2404.imagepicker.ImagePicker
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityProfileUpdateBinding
import kr.co.vividnext.sodalive.databinding.ItemLiveTagSelectedBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.mypage.profile.nickname.NicknameUpdateActivity
import kr.co.vividnext.sodalive.mypage.profile.password.ModifyPasswordActivity
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagFragment
import kr.co.vividnext.sodalive.user.Gender
import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit
class ProfileUpdateActivity : BaseActivity<ActivityProfileUpdateBinding>(
ActivityProfileUpdateBinding::inflate
) {
private val viewModel: ProfileUpdateViewModel by inject()
private val imageResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
// Image Uri will not be null for RESULT_OK
val fileUri = data?.data!!
binding.ivProfile.background = null
viewModel.updateProfileImage(fileUri) {
binding.ivProfile.load(it) {
crossfade(true)
transformations(CircleCropTransformation())
}
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
}
}
private val tagFragment: MemberTagFragment by lazy {
MemberTagFragment(viewModel.tags) { tag, isChecked ->
when {
isChecked -> {
viewModel.addTag(tag)
return@MemberTagFragment true
}
!isChecked -> {
viewModel.removeTag(tag)
return@MemberTagFragment true
}
else -> {
return@MemberTagFragment false
}
}
}
}
private lateinit var loadingDialog: LoadingDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.getRealPathFromURI = {
RealPathUtil.getRealPath(applicationContext, it)
}
bindData()
}
override fun onStart() {
super.onStart()
viewModel.getUserInfo()
}
private fun bindData() {
compositeDisposable.add(
binding.etBlog.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.blogUrl = it.toString()
}
)
compositeDisposable.add(
binding.etWebsite.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.websiteUrl = it.toString()
}
)
compositeDisposable.add(
binding.etYoutube.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.youtubeUrl = it.toString()
}
)
compositeDisposable.add(
binding.etInstagram.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.instagramUrl = it.toString()
}
)
viewModel.genderLiveData.observe(this) {
binding.tvMale.isSelected = false
binding.tvFemale.isSelected = false
binding.tvNone.isSelected = false
when (it) {
Gender.MALE -> binding.tvMale.isSelected = true
Gender.FEMALE -> binding.tvFemale.isSelected = true
Gender.NONE -> binding.tvNone.isSelected = true
else -> {
}
}
}
viewModel.userInfoLiveData.observe(this) {
binding.ivProfile.load(it.profileUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
binding.tvEmail.text = it.email
binding.tvNickname.text = it.nickname
it.youtubeUrl?.let { url -> binding.etYoutube.setText(url) }
it.instagramUrl?.let { url -> binding.etInstagram.setText(url) }
it.websiteUrl?.let { url -> binding.etWebsite.setText(url) }
it.blogUrl?.let { url -> binding.etBlog.setText(url) }
binding.etIntroduce.setText(it.introduce)
SharedPreferenceManager.nickname = it.nickname
}
viewModel.toastLiveData.observe(this) {
it?.let {
Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show()
}
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.selectedTagLiveData.observe(this) {
binding.llSelectTags.removeAllViews()
binding.llSelectTags.visibility = if (it.isNotEmpty()) {
View.VISIBLE
} else {
View.GONE
}
for (index in it.indices) {
val tag = it[index]
val itemView = ItemLiveTagSelectedBinding.inflate(layoutInflater)
itemView.tvTag.text = tag
itemView.ivRemove.setOnClickListener {
viewModel.removeTag(tag)
}
binding.llSelectTags.addView(itemView.root)
if (index > 0) {
val layoutParams = itemView.root.layoutParams as LinearLayout.LayoutParams
layoutParams.marginStart = 10.dpToPx().toInt()
itemView.root.layoutParams = layoutParams
}
}
}
}
override fun setupView() {
binding.toolbar.tvBack.text = "프로필 수정"
binding.toolbar.tvBack.setOnClickListener { finish() }
loadingDialog = LoadingDialog(this, layoutInflater)
compositeDisposable.add(
binding.etIntroduce.textChanges()
.debounce(500, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.introduce = it.toString()
}
)
binding.tvMale.setOnClickListener {
viewModel.changeGender(Gender.MALE)
}
binding.tvFemale.setOnClickListener {
viewModel.changeGender(Gender.FEMALE)
}
binding.tvNone.setOnClickListener {
viewModel.changeGender(Gender.NONE)
}
binding.ivPhotoPicker.setOnClickListener {
ImagePicker.with(this)
.crop()
.galleryOnly()
.galleryMimeTypes( // Exclude gif images
mimeTypes = arrayOf(
"image/png",
"image/jpg",
"image/jpeg"
)
)
.createIntent { imageResult.launch(it) }
}
binding.tvSelectTag.setOnClickListener {
if (tagFragment.isAdded) return@setOnClickListener
tagFragment.show(supportFragmentManager, tagFragment.tag)
}
binding.tvModifyPassword.setOnClickListener {
startActivity(
Intent(
applicationContext,
ModifyPasswordActivity::class.java
)
)
}
binding.tvSave.setOnClickListener {
viewModel.updateProfile {
finish()
}
}
binding.tvChangeNickname.setOnClickListener {
startActivity(
Intent(
applicationContext,
NicknameUpdateActivity::class.java
)
)
}
}
}

View File

@ -0,0 +1,20 @@
package kr.co.vividnext.sodalive.mypage.profile
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.user.Gender
data class ProfileUpdateRequest(
@SerializedName("email") val email: String,
@SerializedName("password") val password: String? = null,
@SerializedName("modifyPassword") val modifyPassword: String? = null,
@SerializedName("nickname") val nickname: String? = null,
@SerializedName("gender") val gender: Gender? = null,
@SerializedName("insertTags") val insertTags: List<String>? = null,
@SerializedName("removeTags") val removeTags: List<String>? = null,
@SerializedName("introduce") val introduce: String? = null,
@SerializedName("youtubeUrl") val youtubeUrl: String? = null,
@SerializedName("instagramUrl") val instagramUrl: String? = null,
@SerializedName("websiteUrl") val websiteUrl: String? = null,
@SerializedName("blogUrl") val blogUrl: String? = null,
@SerializedName("container") val container: String = "aos"
)

View File

@ -0,0 +1,290 @@
package kr.co.vividnext.sodalive.mypage.profile
import android.net.Uri
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
import kr.co.vividnext.sodalive.user.Gender
import kr.co.vividnext.sodalive.user.UserRepository
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
class ProfileUpdateViewModel(private val repository: UserRepository) : BaseViewModel() {
var youtubeUrl = ""
var instagramUrl = ""
var websiteUrl = ""
var blogUrl = ""
var introduce = ""
var currentPassword = ""
var newPassword = ""
var newPasswordConfirm = ""
val tags = mutableSetOf<String>()
private val insertTags = mutableListOf<String>()
private val removeTags = mutableListOf<String>()
private lateinit var profileResponse: ProfileResponse
private val _userInfoLiveData = MutableLiveData<ProfileResponse>()
val userInfoLiveData: LiveData<ProfileResponse>
get() = _userInfoLiveData
private val _genderLiveData = MutableLiveData<Gender>()
val genderLiveData: LiveData<Gender>
get() = _genderLiveData
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _selectedTagLiveData = MutableLiveData<List<String>>()
val selectedTagLiveData: LiveData<List<String>>
get() = _selectedTagLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
lateinit var getRealPathFromURI: (Uri) -> String?
fun getUserInfo() {
compositeDisposable.add(
repository.getProfile("Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
profileResponse = it.data
tags.addAll(profileResponse.tags)
_selectedTagLiveData.postValue(profileResponse.tags)
_genderLiveData.postValue(profileResponse.gender)
_userInfoLiveData.postValue(profileResponse)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun changeGender(gender: Gender) {
_genderLiveData.postValue(gender)
}
fun updateProfileImage(uri: Uri, onSuccess: (String) -> Unit) {
val file = File(getRealPathFromURI(uri))
val image = MultipartBody.Part.createFormData(
"image",
file.name,
file.asRequestBody("image/*".toMediaType())
)
compositeDisposable.add(
repository.updateProfileImage(image, "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
onSuccess(it.data)
_toastLiveData.postValue("프로필 이미지가 변경되었습니다.")
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun updateProfile(onSuccess: () -> Unit) {
if (
profileResponse.youtubeUrl != youtubeUrl ||
profileResponse.instagramUrl != instagramUrl ||
profileResponse.blogUrl != blogUrl ||
profileResponse.websiteUrl != websiteUrl ||
profileResponse.gender != _genderLiveData.value ||
insertTags.isNotEmpty() ||
removeTags.isNotEmpty() ||
profileResponse.introduce != introduce
) {
val request = ProfileUpdateRequest(
email = profileResponse.email,
nickname = null,
youtubeUrl = if (profileResponse.youtubeUrl != youtubeUrl) {
youtubeUrl
} else {
null
},
instagramUrl = if (profileResponse.instagramUrl != instagramUrl) {
instagramUrl
} else {
null
},
blogUrl = if (profileResponse.blogUrl != blogUrl) {
blogUrl
} else {
null
},
websiteUrl = if (profileResponse.websiteUrl != websiteUrl) {
websiteUrl
} else {
null
},
gender = if (profileResponse.gender != _genderLiveData.value) {
_genderLiveData.value
} else {
null
},
introduce = if (profileResponse.introduce != introduce) {
introduce
} else {
null
},
insertTags = insertTags.ifEmpty { null },
removeTags = removeTags.ifEmpty { null }
)
_isLoading.value = true
compositeDisposable.add(
repository.updateProfile(request, "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
_toastLiveData.postValue(
"프로필이 변경되었습니다."
)
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
} else run {
onSuccess()
}
}
fun updatePassword(onSuccess: () -> Unit) {
val email = SharedPreferenceManager.email
if (currentPassword.isBlank()) {
_toastLiveData.postValue("현재 비밀번호를 입력하세요")
return
}
if (newPassword.isBlank()) {
_toastLiveData.postValue("변경할 비밀번호를 입력하세요")
return
}
if (newPasswordConfirm != newPassword) {
_toastLiveData.postValue("비밀번호가 일치하지 않습니다.")
return
}
val request = ProfileUpdateRequest(
email = email,
password = currentPassword,
modifyPassword = newPassword
)
_isLoading.value = true
compositeDisposable.add(
repository.updateProfile(request, "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
_toastLiveData.postValue(
"비밀번호가 변경되었습니다."
)
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun removeTag(tag: String) {
tags.remove(tag)
if (insertTags.contains(tag)) {
insertTags.remove(tag)
} else {
removeTags.add(tag)
}
_selectedTagLiveData.postValue(tags.toList())
}
fun addTag(tag: String) {
tags.add(tag)
if (removeTags.contains(tag)) {
removeTags.remove(tag)
} else {
insertTags.add(tag)
}
_selectedTagLiveData.postValue(tags.toList())
}
}

View File

@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.mypage.profile.nickname
import com.google.gson.annotations.SerializedName
data class GetChangeNicknamePriceResponse(@SerializedName("price") val price: Int)

View File

@ -0,0 +1,83 @@
package kr.co.vividnext.sodalive.mypage.profile.nickname
import android.annotation.SuppressLint
import android.os.Bundle
import android.widget.Toast
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityNicknameUpdateBinding
import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit
class NicknameUpdateActivity : BaseActivity<ActivityNicknameUpdateBinding>(
ActivityNicknameUpdateBinding::inflate
) {
private val viewModel: NicknameUpdateViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.getChangeNicknamePrice()
binding.etNickname.setText(SharedPreferenceManager.nickname)
}
override fun setupView() {
binding.toolbar.tvBack.text = "닉네임 변경"
binding.toolbar.tvBack.setOnClickListener { finish() }
loadingDialog = LoadingDialog(this, layoutInflater)
binding.tvCheckNickname.setOnClickListener {
viewModel.checkNickname()
}
binding.tvChangeNickname.setOnClickListener {
viewModel.changeNickname { finish() }
}
}
@SuppressLint("SetTextI18n")
private fun bindData() {
compositeDisposable.add(
binding.etNickname.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.isCheckedNickname = false
viewModel.nickname = it.toString()
}
)
viewModel.toastLiveData.observe(this) {
it?.let {
Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show()
}
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.priceLiveData.observe(this) {
if (it > 0) {
binding.tvChangeNickname.text = "${it}캔으로 닉네임 변경하기"
} else {
binding.tvChangeNickname.text = "닉네임 변경하기"
}
}
}
}

View File

@ -0,0 +1,142 @@
package kr.co.vividnext.sodalive.mypage.profile.nickname
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
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateRequest
import kr.co.vividnext.sodalive.user.UserRepository
class NicknameUpdateViewModel(private val repository: UserRepository) : BaseViewModel() {
var nickname = ""
private val _priceLiveData = MutableLiveData(0)
val priceLiveData: LiveData<Int>
get() = _priceLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
var isCheckedNickname = false
fun getChangeNicknamePrice() {
_isLoading.value = true
compositeDisposable.add(
repository.getChangeNicknamePrice(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
_priceLiveData.value = it.data.price
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun checkNickname() {
if (nickname.isNotBlank()) {
_isLoading.value = true
compositeDisposable.add(
repository.checkNickname(nickname)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
isCheckedNickname = true
_toastLiveData.postValue("사용가능한 닉네임 입니다.")
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
)
)
} else {
_toastLiveData.postValue("닉네임을 입력하세요.")
}
}
fun changeNickname(onSuccess: () -> Unit) {
if (isCheckedNickname) {
_isLoading.value = true
compositeDisposable.add(
repository.changeNickname(
request = ProfileUpdateRequest(
email = SharedPreferenceManager.email,
nickname = nickname
),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
_toastLiveData.postValue("닉네임이 변경되었습니다.")
SharedPreferenceManager.nickname = nickname
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
)
)
} else {
_toastLiveData.postValue("닉네임 중복체크를 해주세요.")
}
}
}

View File

@ -0,0 +1,72 @@
package kr.co.vividnext.sodalive.mypage.profile.password
import android.os.Bundle
import android.widget.Toast
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.databinding.ActivityModifyPasswordBinding
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateViewModel
import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit
class ModifyPasswordActivity : BaseActivity<ActivityModifyPasswordBinding>(
ActivityModifyPasswordBinding::inflate
) {
private val viewModel: ProfileUpdateViewModel by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
}
private fun bindData() {
compositeDisposable.add(
binding.etCurrentPassword.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.currentPassword = it.toString()
}
)
compositeDisposable.add(
binding.etNewPassword.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.newPassword = it.toString()
}
)
compositeDisposable.add(
binding.etNewPasswordConfirm.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.newPasswordConfirm = it.toString()
}
)
viewModel.toastLiveData.observe(this) {
it?.let {
Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show()
}
}
}
override fun setupView() {
binding.toolbar.tvBack.text = "비밀번호 변경"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.tvModifyPassword.setOnClickListener {
viewModel.updatePassword { finish() }
}
}
}

View File

@ -0,0 +1,76 @@
package kr.co.vividnext.sodalive.mypage.profile.tag
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemLiveTagBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class MemberTagAdapter(
private val selectedTags: Set<String>,
private val onItemClick: (String, Boolean) -> Boolean
) : RecyclerView.Adapter<MemberTagAdapter.ViewHolder>() {
inner class ViewHolder(
private val binding: ItemLiveTagBinding
) : RecyclerView.ViewHolder(binding.root) {
private var isChecked = false
fun bind(item: MemberTagResponse) {
if (selectedTags.contains(item.tag)) {
binding.ivTagChecked.visibility = View.VISIBLE
binding.ivTag.setBackgroundResource(R.drawable.bg_round_corner_30_9970ff)
isChecked = true
} else {
binding.ivTagChecked.visibility = View.GONE
binding.ivTag.background = null
isChecked = false
}
binding.ivTag.load(item.image) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(30f.dpToPx()))
}
binding.tvTag.text = item.tag
binding.root.setOnClickListener {
isChecked = !isChecked
if (onItemClick(item.tag, isChecked)) {
if (isChecked) {
binding.ivTagChecked.visibility = View.VISIBLE
binding.ivTag.setBackgroundResource(R.drawable.bg_round_corner_30_9970ff)
} else {
binding.ivTagChecked.visibility = View.GONE
binding.ivTag.background = null
}
} else {
isChecked = !isChecked
}
}
}
}
val items = mutableSetOf<MemberTagResponse>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemLiveTagBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items.toList()[position])
}
override fun getItemCount() = items.size
}

View File

@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.mypage.profile.tag
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import retrofit2.http.GET
import retrofit2.http.Header
interface MemberTagApi {
@GET("/member/tag")
fun getTags(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<MemberTagResponse>>>
}

View File

@ -0,0 +1,117 @@
package kr.co.vividnext.sodalive.mypage.profile.tag
import android.annotation.SuppressLint
import android.app.Dialog
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class MemberTagFragment(
private val selectedTags: Set<String>,
private val onItemClick: (String, Boolean) -> Boolean
) : BottomSheetDialogFragment() {
private val viewModel: MemberTagViewModel by inject()
private lateinit var adapter: MemberTagAdapter
private lateinit var loadingDialog: LoadingDialog
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.setOnShowListener {
val d = it as BottomSheetDialog
val bottomSheet = d.findViewById<FrameLayout>(
com.google.android.material.R.id.design_bottom_sheet
)
if (bottomSheet != null) {
BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_EXPANDED
}
}
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_creator_tag, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<ImageView>(R.id.iv_close).setOnClickListener {
dialog?.dismiss()
}
view.findViewById<TextView>(R.id.tv_select).setOnClickListener {
dialog?.dismiss()
}
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupAdapter(view)
bindData()
viewModel.getTags()
}
private fun setupAdapter(view: View) {
val recyclerView = view.findViewById<RecyclerView>(R.id.rv_tags)
adapter = MemberTagAdapter(selectedTags) { tag, isChecked ->
return@MemberTagAdapter onItemClick(tag, isChecked)
}
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = GridLayoutManager(requireContext(), 4)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
}
})
recyclerView.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
}
viewModel.tagLiveData.observe(viewLifecycleOwner) {
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(resources.displayMetrics.widthPixels)
} else {
loadingDialog.dismiss()
}
}
}
}

View File

@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.mypage.profile.tag
class MemberTagRepository(private val api: MemberTagApi) {
fun getTags(token: String) = api.getTags(authHeader = token)
}

Some files were not shown because too many files have changed in this diff Show More