Compare commits

129 Commits

Author SHA1 Message Date
7e32727773 version 10(1.1.0) 2023-11-05 00:47:11 +09:00
1a3396b293 콘텐츠 구매 - 캔이 부족하면 캔 충전 페이지로 이동하도록 수정 2023-11-03 20:37:34 +09:00
d883a81602 콘텐츠 메인 - 인기 콘텐츠 조회 page 0->1 변경 2023-11-03 18:23:47 +09:00
81bdd52edd 인기 콘텐츠 전체 보기 - 정렬 추가 2023-11-02 21:05:39 +09:00
63483a2099 콘텐츠 메인 - 인기 콘텐츠 정렬 추가 2023-11-02 17:39:50 +09:00
b68b4eb8da version 9 (1.0.8) 2023-10-31 22:13:02 +09:00
d8cc218139 구매 목록 - 구매한 콘텐츠 총 개수 표시 2023-10-31 18:35:07 +09:00
0226a696d4 라이브 - 방장을 제외한 모든 유저에게 참여자 목록 버튼이 보이지 않도록 수정 2023-10-31 13:56:50 +09:00
c9eb0f0e01 구매 목록 - 페이징을 추가해 이전 구매목록이 추가로 로딩되도록 수정 2023-10-31 12:23:43 +09:00
5c293e79cf 구매 목록 - 페이징을 추가해 이전 구매목록이 추가로 로딩되도록 수정 2023-10-31 12:23:12 +09:00
5cdb7426c6 구매 목록 - 페이징을 추가해 이전 구매목록이 추가로 로딩되도록 수정 2023-10-30 22:33:36 +09:00
cc73f471d2 카울리 - 무료 충전 버튼 크기 수정, 캔 내역에 무료 충전 버튼 추가 2023-10-27 22:22:58 +09:00
dc3240f224 카울리 무료 충전 버튼 보이게 수정 2023-10-27 02:06:21 +09:00
04eda1ffbd 결제수단 - 휴대폰 결제 추가 2023-10-27 02:02:24 +09:00
39d790c1c3 사용하지 않는 isAdult 제거 2023-10-24 23:22:21 +09:00
3e3d0de7ca 카울리 PointClick SDK verion 1.0.17로 변경 2023-10-23 16:58:25 +09:00
165b75487b 콘텐츠 등록 - 유료 콘텐츠의 경우에만 미리듣기 시간설정을 할 수 있도록 수정 2023-10-22 17:59:50 +09:00
19b351ef2a 콘텐츠 등록 - 대여만 가능한 콘텐츠 등록 기능 추가 2023-10-21 00:56:15 +09:00
83575aa1eb 콘텐츠 상세 - 대여만 가능한 콘텐츠의 경우 소장 버튼이 보이지 않고 가격의 100%가 보이도록 수정 2023-10-20 23:13:16 +09:00
26c9a236ec 결제수단 - 휴대폰 결제 숨김 2023-10-20 17:20:03 +09:00
da7f72544f 결제수단 - 휴대폰 결제 추가 2023-10-20 16:24:49 +09:00
444f031f57 콘텐츠 메인 인기 콘텐츠 - 아이템 width 고정 2023-10-15 07:17:09 +09:00
3e7d06a2aa 탐색 - 인기 크리에이터 설명 글 UI 수정 2023-10-15 06:53:39 +09:00
e6b8e55966 인기 콘텐츠 전체 보기 페이지 추가 2023-10-15 04:38:07 +09:00
fe1a1cc3cb 콘텐츠 메인 - 인기 콘텐츠 영역 추가 2023-10-15 03:09:47 +09:00
2f17e04e1e 탐색 - 크리에이터 랭킹 UI 추가 2023-10-14 18:48:02 +09:00
41d175a19f admob 제거 2023-10-14 17:08:47 +09:00
8266167c02 탐색 - 섹션 제목 아래에 description 추가 2023-10-14 01:38:00 +09:00
2cfc4b97f4 채금 다이얼로그 - 취소 버튼 동작 추가 2023-10-11 16:55:19 +09:00
fbad5f9d98 채금 기능 추가 2023-10-11 02:49:52 +09:00
ac6b0c52d0 라이브 방 - 팔로잉 버튼 위치 수정 2023-10-06 20:05:26 +09:00
413c526a6a 라이브 지금 예약 중 아이템 - 이미지 RoundedCorners 추가 2023-10-06 19:19:14 +09:00
622021913d 메시지 발송 버튼 색상 변경 2023-10-06 17:45:47 +09:00
5ed5a86e0d 라이브 예약중 전체보기 - 캘린더 선택된 날짜 배경색 변경 2023-10-06 17:34:47 +09:00
3bf4f273d2 라이브 지금 예약중 - 라이브 커버 이미지 사이즈가 작게 보이던 버그 수정 2023-10-06 17:31:31 +09:00
0e6c78a6c0 유료 콘텐츠 미리 듣기 재생 버튼 추가 2023-10-05 23:24:10 +09:00
71cd52d30a 후원랭킹 전체보기 후원랭킹 활성화 스위치 - 클릭 리스너 추가 2023-10-05 22:31:15 +09:00
d35b920470 후원랭킹 전체보기 - 후원랭킹 활성화 스위치 추가 2023-10-05 19:13:12 +09:00
5a4355044f 지금 라이브 중 전체 보기 아이템 - 배경 색상 제거 2023-09-27 16:27:42 +09:00
b74d4b18e7 콘텐츠 큐레이션 전체보기 - UI 형태 그리드로 변경 2023-09-27 15:58:30 +09:00
a53b76415b 콘텐츠 큐레이션 전체보기 - UI 형태 그리드로 변경 2023-09-27 15:49:00 +09:00
a286ee760d 콘텐츠 업로드 - 미리듣기 시간 설정 안내 문구 글자 간격 수정 2023-09-27 15:48:02 +09:00
92b72db25c 콘텐츠 업로드 - 미리듣기 시간 설정 안내 문구 추가 2023-09-27 15:42:59 +09:00
eed7bfa158 예약 라이브 전체 보기 - 라이브 만들기 페이지로 이동하는 기능 제거 2023-09-27 14:59:38 +09:00
549644a224 콘텐츠 큐레이션 - 너비가 가득 차도록 수정 2023-09-27 14:51:04 +09:00
ecec8be386 새로운 콘텐츠 전체보기 페이지 추가 2023-09-27 14:19:54 +09:00
46b423e3e6 큐레이션 콘텐츠 전체보기 페이지 추가 2023-09-26 22:04:41 +09:00
1206977907 콘텐츠 사이 배너광고 제거 2023-09-26 16:07:03 +09:00
b38fd26b77 무료 충전 아이콘 숨김 2023-09-22 22:32:30 +09:00
302e7d9a45 콘텐츠 업로드 - 미리 듣기 시간 설정 기능 추가 2023-09-22 18:08:31 +09:00
b7a986c33c checkReleaseBuilds 추가 2023-09-21 22:44:21 +09:00
6fc474cff4 탐색 탭 - 배너 광고 unit id 변경 2023-09-20 18:52:28 +09:00
4bcc1b2680 point click sdk 추가 2023-09-19 22:42:11 +09:00
318bae54a1 versionCode 5, versionName 1.0.4 2023-09-19 15:38:36 +09:00
959d20fe6f 콘텐츠 상세 - 배너 광고 간격 수정 2023-09-16 01:40:36 +09:00
00277117f1 콘텐츠 상세 - 배너 광고 위치 수정 2023-09-16 01:31:08 +09:00
42613dfc76 탐색 - 광고 배너 추가 2023-09-16 01:12:31 +09:00
90df714a44 라이브 메인 - 광고 위치 추천 채널 밑으로 이동 2023-09-15 23:21:36 +09:00
62fc0e1d59 콘텐츠 메인 - 광고 위치 수정 2023-09-15 23:17:28 +09:00
e3679fd1dc 라이브 방 - 배너 광고 제거 2023-09-15 23:06:35 +09:00
9626823f0c 라이브 방 - 배너 광고 추가 2023-09-15 21:35:25 +09:00
9fc795afac binding 버그 수정 2023-09-15 16:33:38 +09:00
6610f13619 라이브 상세 - 배너 광고 추가 2023-09-15 02:29:53 +09:00
f9401d91c4 메시지 - 배너 광고 추가 2023-09-15 02:22:15 +09:00
52e6965472 크리에이터 채널 - 배너 광고 추가 2023-09-15 02:06:27 +09:00
0343c91f1c 라이브 메인, 팔로잉 채널 전체보기, 지금 라이브 중 전체보기 - 배너 광고 추가 2023-09-15 01:54:02 +09:00
cce1b4f446 콘텐츠 메인 - 배너 광고 간격 수정 2023-09-15 01:35:00 +09:00
db1981b5fe 콘텐츠 구매목록 - 배너 광고 추가 2023-09-15 01:32:21 +09:00
cae15b7f39 콘텐츠 메인 - 배너 광고 추가 2023-09-15 01:22:51 +09:00
26e43bd548 콘텐츠 상세 - 배너 광고 추가 2023-09-14 03:14:48 +09:00
f6cbaffd3b 휴대폰 결제 임시로 숨김 2023-09-13 14:43:05 +09:00
4d4ddb50ac 메시지 추가 로딩 되지 않는 버그 수정 2023-09-13 14:31:06 +09:00
9ed175191b 재생수 업데이트 로직 - 10초 이상 연속재생 한 경우 업데이트 하도록 수정 2023-09-13 12:25:41 +09:00
4d5c3acff5 휴대폰 결제 추가 2023-09-12 01:31:02 +09:00
d791147886 @SerializedName 추가 2023-09-09 01:55:14 +09:00
dc028f297d versionName: 1.0.2, versionCode 3 2023-09-09 01:52:52 +09:00
e13eb3c404 응원글 - 수정 기능 추가 2023-09-07 16:35:19 +09:00
b6359f9b8d 콘텐츠 댓글의 답글 - 수정/삭제 기능 추가 2023-09-07 13:37:19 +09:00
5522672195 콘텐츠 댓글 수정 기능 추가 2023-09-07 13:06:35 +09:00
e9370e02be 콘텐츠 댓글 삭제 기능 추가 2023-09-06 13:59:50 +09:00
4f54545081 알림 아이콘 변경 2023-09-06 11:27:26 +09:00
8a4ad41212 응원글 삭제 기능 추가 2023-09-05 12:46:53 +09:00
e371fd2ac2 콘텐츠 구매 목록 - 콘텐츠 크리에이터 닉네임 표시 2023-09-04 22:48:54 +09:00
ad7a5b36f1 푸시, 딥링크 - 라이브 탭으로 이동하지 않아도 실행되도록 수정 2023-08-31 20:04:33 +09:00
9eb136379c 코인 -> 캔 2023-08-29 23:39:10 +09:00
735f353f31 콘텐츠 대여가격 60%로 변경 2023-08-29 23:29:33 +09:00
a422eb08c0 응원 전체보기 추가 2023-08-29 17:31:04 +09:00
d1b97aafd2 후원랭킹 전체보기 추가 2023-08-29 11:24:06 +09:00
110fcab710 콘텐츠 등록 - 5캔(500원) 부터 등록되도록 수정, 대여가격 안내 60%로 수정 2023-08-28 18:19:04 +09:00
91af371523 라이브 방 - 공지사항 수정 가능 2023-08-28 18:10:04 +09:00
2ee4c3e9ad 푸시 메시지 - 채널 id 추가 2023-08-28 17:56:28 +09:00
96c72c4fa0 라이브 후원 다이얼로그 - 키보드가 올라왔을 때 화면이 전체적으로 위로 이동하여 보이지 않는 부분이 없어지도록 수정 2023-08-25 21:56:03 +09:00
0a56ef1227 라이브 방 - 스피커 최대 10 -> 5명으로 수정 2023-08-21 05:37:49 +09:00
6a06a46713 메시지 탭 보이도록 수정 2023-08-21 04:41:44 +09:00
69f2cbd4a2 본인인증을 한 유저만 메시지 탭이 보이도록 수정 2023-08-21 04:20:09 +09:00
f0e841ffbe 라이브 상세 - 제목 왼쪽에 19금 표시 추가 2023-08-21 04:08:37 +09:00
b8d11d2276 로딩뷰 변경 2023-08-21 04:00:11 +09:00
28d5f3a6f6 라이브 메인 - 이벤트 배너 터치이벤트 추가 2023-08-21 01:35:05 +09:00
8e135e893e 라이브 방 - 입장메시지 방장만 보이도록 수정 2023-08-21 01:32:01 +09:00
01512abaf0 라이브 방 - 19금 방인 경우 제목 왼쪽에 19 표시 2023-08-20 23:44:41 +09:00
bb7476f640 마이페이지 탭 - 구매내역 추가 2023-08-20 23:37:17 +09:00
875d8361f3 이용약관/개인정보처리방침 링크 수정 2023-08-20 20:57:52 +09:00
36ffa4fa58 메인 하단 탭 - 콘텐츠, 라이브 순서 변경 2023-08-20 20:46:29 +09:00
3b868294b0 19금 표시 제거 2023-08-20 17:46:16 +09:00
508eff2cd0 크리에이터 채널 - 콘텐츠 영역 추가 2023-08-20 03:46:13 +09:00
9bc625b7a0 socdoc -> message 로 변경 2023-08-19 22:54:38 +09:00
98d895f91f 사용방법 블로그 URL 수정 2023-08-19 22:53:10 +09:00
bf75835c69 ProfileResponse - Coin 을 Can으로 변경 2023-08-19 16:25:15 +09:00
4ac272cd9d 콘텐츠 메인 - SerializeName 변경 audioContents -> contents 2023-08-18 23:14:56 +09:00
a5abb39059 이미지 로딩 라이브러리 변경 2023-08-18 21:51:38 +09:00
f7299cc0df mediaPlayer 를 사용하는 곳은 항상 초기화 되어 있는지 확인 하고 로직이 실행되도록 수정 2023-08-18 21:06:53 +09:00
a4cdbebeb4 본인이 만든 라이브의 경우 팔로잉 버튼, 선물하기 버튼이 보이지 않도록 수정 2023-08-18 20:56:26 +09:00
2e48dad913 라이브 방 상세 - 예약자/참여자 리스트 제거, 채널보기 버튼 액션 추가 2023-08-18 19:51:35 +09:00
f6d6f1de8d 라이브 방 - 키보드가 올라오면 화면이 전체적으로 위로 올라가도록 수정 2023-08-18 19:41:21 +09:00
be7c7d0682 프로필 수정 페이지 추가 2023-08-18 19:39:04 +09:00
9adadaf572 공유링크 수정 2023-08-18 16:54:55 +09:00
ca30e6949f 앱 런처 아이콘 변경 2023-08-18 16:43:35 +09:00
9fa6ccb786 온보딩 튜토리얼 추가 2023-08-18 16:41:36 +09:00
3d96fab0f6 placeholder 이미지 변경 2023-08-17 23:59:14 +09:00
0524615ee4 소다라이브 사용방법 배너 마이페이지로 이동 2023-08-17 23:54:47 +09:00
e165813545 캔 아이콘 변경 2023-08-17 23:41:16 +09:00
7aaac27d24 고객센터 카톡 URL 변경 2023-08-17 14:03:56 +09:00
66006853bd AGP 7.3.1 로 변경 2023-08-17 13:53:48 +09:00
79a241f436 proguard rule 추가 2023-08-16 19:09:52 +09:00
0e24eab7b3 firebase version 32.2.2 로 변경 2023-08-16 18:21:03 +09:00
ea79ffcc02 add real service firebase config file 2023-08-16 18:11:47 +09:00
90655c692d manager -> creator 2023-08-15 00:11:52 +09:00
7e9bcb6c38 사용하지 않는 필드 제거 2023-08-14 22:56:57 +09:00
999f90ae80 coin -> can,
코인 -> 캔

으로 변경
2023-08-14 11:15:21 +09:00
271 changed files with 8877 additions and 1563 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 {
@@ -26,6 +26,7 @@ android {
lintOptions {
checkDependencies true
checkReleaseBuilds false
}
dependenciesInfo {
@@ -39,8 +40,8 @@ android {
applicationId "kr.co.vividnext.sodalive"
minSdk 23
targetSdk 33
versionCode 1
versionName "1.0.0"
versionCode 10
versionName "1.1.0"
}
buildTypes {
@@ -98,13 +99,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 +114,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 +127,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'
@@ -148,4 +149,7 @@ dependencies {
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
implementation "com.michalsvec:single-row-calednar:1.0.0"
// PointClick Maven Remote Repo
implementation 'kr.co.pointclick.sdk.offerwall:pointclick-sdk-offerwall:1.0.17'
}

126
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,12 @@
-keep class androidx.recyclerview.widget.**{*;}
-keep class androidx.viewpager2.widget.**{*;}
-keep class kr.co.bootpay.core.** { *; }
-keep class kr.co.pointclick.sdk.offerwall.core.consts.** {*;}
-keep interface kr.co.pointclick.sdk.offerwall.core.consts.** {*;}
-keep class kr.co.pointclick.sdk.offerwall.core.models.** {*;}
-keep interface kr.co.pointclick.sdk.offerwall.core.models.** {*;}
-keep class kr.co.pointclick.sdk.offerwall.core.PointClickAd {*;}
-keep class kr.co.pointclick.sdk.offerwall.core.events.PackageReceiver {*;}

View File

@@ -28,6 +28,21 @@
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<application
android:name=".app.SodaLiveApp"
android:allowBackup="true"
@@ -50,6 +65,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 +88,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 +117,13 @@
<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=".audio_content.curation.AudioContentCurationActivity" />
<activity android:name=".audio_content.all.AudioContentNewAllActivity" />
<activity android:name=".audio_content.all.AudioContentRankingAllActivity" />
<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

@@ -1,14 +1,18 @@
package kr.co.vividnext.sodalive.audio_content
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audio_content.all.GetNewContentAllResponse
import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListResponse
import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest
import kr.co.vividnext.sodalive.audio_content.comment.RegisterAudioContentCommentRequest
import kr.co.vividnext.sodalive.audio_content.curation.GetCurationContentResponse
import kr.co.vividnext.sodalive.audio_content.detail.GetAudioContentDetailResponse
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeResponse
import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationRequest
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRanking
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListResponse
import kr.co.vividnext.sodalive.audio_content.order.OrderRequest
import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse
@@ -132,9 +136,50 @@ interface AudioContentApi {
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@GET("/audio-content/main/new/all")
fun getNewContentAllOfTheme(
@Query("theme") theme: String,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetNewContentAllResponse>>
@POST("/audio-content/donation")
fun donation(
@Body request: AudioContentDonationRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@PUT("/audio-content/comment")
fun modifyComment(
@Body request: ModifyCommentRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/curation/{id}")
fun getAudioContentListByCurationId(
@Path("id") id: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort-type") sort: AudioContentViewModel.Sort,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetCurationContentResponse>>
@GET("/audio-content/main/theme")
fun getNewContentThemeList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<String>>>
@GET("/audio-content/ranking-sort-type")
fun getContentRankingSortType(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<String>>>
@GET("/audio-content/ranking")
fun getContentRanking(
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort-type") sortType: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentRanking>>
}

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

@@ -14,6 +14,20 @@ class AudioContentRepository(
private val api: AudioContentApi,
private val userApi: UserApi
) {
fun getAudioContentListByCurationId(
curationId: Long,
page: Int,
size: Int,
sort: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
token: String
) = api.getAudioContentListByCurationId(
id = curationId,
page = page - 1,
size = size,
sort = sort,
authHeader = token
)
fun getAudioContentList(
id: Long,
page: Int,
@@ -122,6 +136,20 @@ class AudioContentRepository(
authHeader = token
)
fun getNewContentAllOfTheme(
theme: String,
page: Int,
size: Int,
token: String
) = api.getNewContentAllOfTheme(
theme = theme,
page = page - 1,
size = size,
authHeader = token
)
fun getNewContentThemeList(token: String) = api.getNewContentThemeList(authHeader = token)
fun donation(
contentId: Long,
can: Int,
@@ -135,4 +163,18 @@ class AudioContentRepository(
),
authHeader = token
)
fun getContentRankingSortType(token: String) = api.getContentRankingSortType(authHeader = token)
fun getContentRanking(
page: Int,
size: Int,
sortType: String = "매출",
token: String
) = api.getContentRanking(
page = page - 1,
size = size,
sortType = sortType,
authHeader = token
)
}

View File

@@ -0,0 +1,194 @@
package kr.co.vividnext.sodalive.audio_content.all
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainNewContentThemeAdapter
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.ActivityAudioContentNewAllBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBinding>(
ActivityAudioContentNewAllBinding::inflate
) {
private val viewModel: AudioContentNewAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentAdapter: AudioContentNewAllAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.getThemeList()
viewModel.getNewContentList()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "새로운 콘텐츠"
binding.toolbar.tvBack.setOnClickListener { finish() }
setupNewContentTheme()
setupNewContent()
}
private fun setupNewContentTheme() {
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
newContentAdapter.clear()
viewModel.selectTheme(it)
}
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContentTheme.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)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
newContentThemeAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvNewContentTheme.adapter = newContentThemeAdapter
}
private fun setupNewContent() {
newContentAdapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - 40f.dpToPx().toInt()) / 2,
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(this, UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvContent.layoutManager = GridLayoutManager(this, 2)
binding.rvContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
if (position % 2 == 0) {
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
} else {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
}
when (position) {
0, 1 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
newContentAdapter.itemCount - 1, newContentAdapter.itemCount - 2 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getNewContentList()
}
}
})
binding.rvContent.adapter = newContentAdapter
}
private fun bindData() {
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.themeListLiveData.observe(this) {
newContentThemeAdapter.addItems(it)
}
viewModel.newContentListLiveData.observe(this) {
newContentAdapter.addItems(it)
}
viewModel.newContentTotalCountLiveData.observe(this) {
binding.tvTotalCount.text = "$it"
}
}
}

View File

@@ -0,0 +1,85 @@
package kr.co.vividnext.sodalive.audio_content.all
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.databinding.ItemAudioContentNewAllBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentNewAllAdapter(
private val itemWidth: Int,
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit,
) : RecyclerView.Adapter<AudioContentNewAllAdapter.ViewHolder>() {
inner class ViewHolder(
private val binding: ItemAudioContentNewAllBinding,
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentMainItem) {
binding.ivAudioContentCoverImage.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(2.7f.dpToPx()))
val layoutParams = binding.ivAudioContentCoverImage
.layoutParams as ConstraintLayout.LayoutParams
layoutParams.width = itemWidth
layoutParams.height = itemWidth
binding.ivAudioContentCoverImage.layoutParams = layoutParams
}
binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvAudioContentTitle.text = item.title
binding.tvAudioContentCreatorNickname.text = item.creatorNickname
binding.ivAudioContentCreator.setOnClickListener { onClickCreator(item.creatorId) }
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}
private val items = mutableListOf<GetAudioContentMainItem>()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = ViewHolder(
ItemAudioContentNewAllBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
onClickItem = onClickItem,
onClickCreator = onClickCreator
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: AudioContentNewAllAdapter.ViewHolder, position: Int) {
holder.bind(items[position])
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetAudioContentMainItem>) {
this.items.addAll(items)
notifyDataSetChanged()
}
fun clear() {
this.items.clear()
}
}

View File

@@ -0,0 +1,127 @@
package kr.co.vividnext.sodalive.audio_content.all
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.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentNewAllViewModel(
private val repository: AudioContentRepository
) : 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 _themeListLiveData = MutableLiveData<List<String>>()
val themeListLiveData: LiveData<List<String>>
get() = _themeListLiveData
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _newContentListLiveData
private var _newContentTotalCountLiveData = MutableLiveData<Int>()
val newContentTotalCountLiveData: LiveData<Int>
get() = _newContentTotalCountLiveData
private var isLast = false
private var page = 1
private val size = 10
private var selectedTheme = ""
fun getNewContentList() {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getNewContentAllOfTheme(
theme = if (selectedTheme == "전체") {
""
} else {
selectedTheme
},
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
page += 1
_newContentListLiveData.postValue(it.data.items)
_newContentTotalCountLiveData.postValue(it.data.totalCount)
} 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 getThemeList() {
compositeDisposable.add(
repository.getNewContentThemeList(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val themeList = listOf("전체").union(it.data).toList()
_themeListLiveData.postValue(themeList)
} 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 selectTheme(theme: String) {
isLast = false
page = 1
selectedTheme = theme
getNewContentList()
}
}

View File

@@ -0,0 +1,185 @@
package kr.co.vividnext.sodalive.audio_content.all
import android.annotation.SuppressLint
import android.content.Intent
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.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainNewContentThemeAdapter
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.ActivityAudioContentRankingAllBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentRankingAllActivity : BaseActivity<ActivityAudioContentRankingAllBinding>(
ActivityAudioContentRankingAllBinding::inflate
) {
private val viewModel: AudioContentRankingAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentRankingAllAdapter
private lateinit var sortAdapter: AudioContentMainNewContentThemeAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.getAudioContentRankingSortType()
viewModel.getAudioContentRanking()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.toolbar.tvBack.text = "인기 콘텐츠"
adapter = AudioContentRankingAllAdapter {
val intent = Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
startActivity(intent)
}
binding.rvContentRanking.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
binding.rvContentRanking.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 0
outRect.bottom = 10f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 10f.dpToPx().toInt()
outRect.bottom = 0
}
else -> {
outRect.top = 10f.dpToPx().toInt()
outRect.bottom = 10f.dpToPx().toInt()
}
}
}
})
binding.rvContentRanking.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getAudioContentRanking()
}
}
})
binding.rvContentRanking.adapter = adapter
setupContentRankingSortType()
}
@SuppressLint("NotifyDataSetChanged")
private fun setupContentRankingSortType() {
sortAdapter = AudioContentMainNewContentThemeAdapter {
adapter.items.clear()
adapter.notifyDataSetChanged()
viewModel.selectSort(sort = it)
}
binding.rvContentRankingSort.layoutManager = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvContentRankingSort.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)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
sortAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvContentRankingSort.adapter = sortAdapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.dateStringLiveData.observe(this) {
binding.tvDate.text = it
}
viewModel.contentRankingItemsLiveData.observe(this) {
if (viewModel.page == 2) {
adapter.items.clear()
}
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
viewModel.contentRankingSortListLiveData.observe(this) {
sortAdapter.addItems(it)
}
}
}

View File

@@ -0,0 +1,83 @@
package kr.co.vividnext.sodalive.audio_content.all
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.databinding.ItemAudioContentRankingAllBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentRankingAllAdapter(
private val onItemClick: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentRankingAllAdapter.ViewHolder>() {
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentRankingAllBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentRankingItem, index: Int) {
binding.root.setOnClickListener { onItemClick(item.contentId) }
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
binding.tvTitle.text = item.title
binding.tvRank.text = index.plus(1).toString()
binding.tvTheme.text = item.themeStr
binding.tvDuration.text = item.duration
binding.tvNickname.text = item.creatorNickname
if (item.price < 1) {
binding.tvPrice.text = "무료"
binding.tvPrice.setTextColor(ContextCompat.getColor(context, R.color.white))
binding.tvPrice.setCompoundDrawables(null, null, null, null)
binding.tvPrice.setPadding(
5.3f.dpToPx().toInt(),
2.7f.dpToPx().toInt(),
5.3f.dpToPx().toInt(),
2.7f.dpToPx().toInt()
)
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_2_6_cf5c37)
} else {
binding.tvPrice.text = item.price.moneyFormat()
binding.tvPrice.setTextColor(ContextCompat.getColor(context, R.color.color_909090))
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_can,
0,
0,
0
)
binding.tvPrice.setPadding(0, 0, 0, 0)
binding.tvPrice.setBackgroundResource(0)
}
}
}
val items = mutableListOf<GetAudioContentRankingItem>()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = ViewHolder(
parent.context,
ItemAudioContentRankingAllBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], position)
}
}

View File

@@ -0,0 +1,120 @@
package kr.co.vividnext.sodalive.audio_content.all
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.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentRankingAllViewModel(
private val repository: AudioContentRepository
) : 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 _dateStringLiveData = MutableLiveData<String>()
val dateStringLiveData: LiveData<String>
get() = _dateStringLiveData
private var _contentRankingSortListLiveData = MutableLiveData<List<String>>()
val contentRankingSortListLiveData: LiveData<List<String>>
get() = _contentRankingSortListLiveData
private var _contentRankingItemsLiveData = MutableLiveData<List<GetAudioContentRankingItem>>()
val contentRankingItemsLiveData: LiveData<List<GetAudioContentRankingItem>>
get() = _contentRankingItemsLiveData
var page = 1
private var pageSize = 10
private var isLast = false
private var selectedSort = "매출"
fun getAudioContentRanking() {
if (!_isLoading.value!! && !isLast && page <= 5) {
_isLoading.value = true
compositeDisposable.add(
repository.getContentRanking(
page = page,
size = pageSize,
sortType = selectedSort,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_isLoading.value = false
_dateStringLiveData.value =
"${it.data.startDate}~${it.data.endDate}"
if (it.data.items.isNotEmpty()) {
page += 1
_contentRankingItemsLiveData.value = it.data.items
} else {
isLast = true
}
} else {
_isLoading.value = false
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
fun getAudioContentRankingSortType() {
compositeDisposable.add(
repository.getContentRankingSortType(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_contentRankingSortListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun selectSort(sort: String) {
page = 1
isLast = false
selectedSort = sort
getAudioContentRanking()
}
}

View File

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.all
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
data class GetNewContentAllResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentMainItem>
)

View File

@@ -1,24 +1,31 @@
package kr.co.vividnext.sodalive.audio_content.comment
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.appcompat.widget.PopupMenu
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.ItemAudioContentCommentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentCommentAdapter(
private val creatorId: Long,
private val modifyComment: (Long, String) -> Unit,
private val onClickDelete: (Long) -> Unit,
private val onItemClick: (GetAudioContentCommentListItem) -> Unit
) : RecyclerView.Adapter<AudioContentCommentAdapter.ViewHolder>() {
var items = mutableSetOf<GetAudioContentCommentListItem>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentCommentBinding
) : RecyclerView.ViewHolder(binding.root) {
@@ -30,34 +37,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 +75,7 @@ class AudioContentCommentAdapter(
)
} else {
tvCommentLayoutParams.topMargin = 13.3f.dpToPx().toInt()
binding.llDonationCoin.visibility = View.GONE
binding.llDonationCan.visibility = View.GONE
}
binding.tvComment.layoutParams = tvCommentLayoutParams
@@ -82,12 +89,42 @@ class AudioContentCommentAdapter(
"답글 쓰기"
}
if (
item.writerId == SharedPreferenceManager.userId ||
creatorId == SharedPreferenceManager.userId
) {
binding.etCommentModify.setText(item.comment)
binding.ivMenu.visibility = View.VISIBLE
binding.ivMenu.setOnClickListener {
showOptionMenu(
context,
binding.ivMenu,
commentId = item.id,
writerId = item.writerId,
creatorId = creatorId,
onClickModify = {
binding.rlCommentModify.visibility = View.VISIBLE
binding.tvComment.visibility = View.GONE
}
)
}
binding.tvModify.setOnClickListener {
binding.rlCommentModify.visibility = View.GONE
binding.tvComment.visibility = View.VISIBLE
modifyComment(item.id, binding.etCommentModify.text.toString())
}
} else {
binding.ivMenu.visibility = View.GONE
}
binding.tvWriteReply.setOnClickListener { onItemClick(item) }
binding.root.setOnClickListener { onItemClick(item) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemAudioContentCommentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
@@ -100,4 +137,38 @@ class AudioContentCommentAdapter(
}
override fun getItemCount() = items.size
private fun showOptionMenu(
context: Context,
v: View,
commentId: Long,
writerId: Long,
creatorId: Long,
onClickModify: () -> Unit
) {
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
if (writerId == SharedPreferenceManager.userId) {
inflater.inflate(R.menu.content_comment_option_menu, popup.menu)
} else if (creatorId == SharedPreferenceManager.userId) {
inflater.inflate(R.menu.content_comment_option_menu2, popup.menu)
}
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_review_modify -> {
onClickModify()
}
R.id.menu_review_delete -> {
onClickDelete(commentId)
}
}
true
}
popup.show()
}
}

View File

@@ -12,7 +12,10 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.DialogAudioContentCommentBinding
class AudioContentCommentFragment(private val audioContentId: Long) : BottomSheetDialogFragment() {
class AudioContentCommentFragment(
private val creatorId: Long,
private val audioContentId: Long
) : BottomSheetDialogFragment() {
private lateinit var binding: DialogAudioContentCommentBinding
@@ -46,6 +49,7 @@ class AudioContentCommentFragment(private val audioContentId: Long) : BottomShee
val commentListFragmentTag = "COMMENT_LIST_FRAGMENT"
val commentListFragment = AudioContentCommentListFragment.newInstance(
creatorId = creatorId,
audioContentId = audioContentId
)
val fragmentTransaction = childFragmentManager.beginTransaction()
@@ -61,6 +65,7 @@ class AudioContentCommentFragment(private val audioContentId: Long) : BottomShee
fun onClickComment(comment: GetAudioContentCommentListItem) {
val commentReplyFragmentTag = "COMMENT_REPLY_FRAGMENT"
val commentReplyFragment = AudioContentCommentReplyFragment.newInstance(
creatorId = creatorId,
audioContentId = audioContentId,
comment = comment
)

View File

@@ -15,6 +15,7 @@ import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
@@ -32,6 +33,7 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentCommentAdapter
private var creatorId: Long = 0
private var audioContentId: Long = 0
override fun onCreateView(
@@ -39,6 +41,7 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
creatorId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID) ?: 0
audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0
return super.onCreateView(inflater, container, savedInstanceState)
}
@@ -63,7 +66,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())
}
@@ -74,9 +77,38 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
viewModel.registerComment(audioContentId, comment)
}
adapter = AudioContentCommentAdapter {
(parentFragment as AudioContentCommentFragment).onClickComment(it)
}
adapter = AudioContentCommentAdapter(
creatorId = creatorId,
modifyComment = { commentId, comment ->
hideKeyboard()
viewModel.modifyComment(
commentId = commentId,
audioContentId = audioContentId,
comment = comment
)
},
onClickDelete = {
SodaDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
title = "댓글 삭제",
desc = "삭제하시겠습니까?",
confirmButtonTitle = "삭제",
confirmButtonClick = {
viewModel.modifyComment(
commentId = it,
audioContentId = audioContentId,
isActive = false
)
},
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
},
onItemClick = {
(parentFragment as AudioContentCommentFragment).onClickComment(it)
}
)
val recyclerView = binding.rvComment
recyclerView.setHasFixedSize(true)
@@ -170,8 +202,9 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
}
companion object {
fun newInstance(audioContentId: Long): AudioContentCommentListFragment {
fun newInstance(creatorId: Long, audioContentId: Long): AudioContentCommentListFragment {
val args = Bundle()
args.putLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, creatorId)
args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
val fragment = AudioContentCommentListFragment()

View File

@@ -48,11 +48,12 @@ class AudioContentCommentListViewModel(
if (it.success && it.data != null) {
_totalCommentCount.postValue(it.data.totalCount)
page += 1
if (it.data.items.isNotEmpty()) {
page += 1
_commentList.postValue(it.data.items)
} else {
isLast = true
_commentList.postValue(listOf())
}
} else {
if (it.message != null) {
@@ -122,4 +123,61 @@ class AudioContentCommentListViewModel(
)
)
}
fun modifyComment(
commentId: Long,
audioContentId: Long,
comment: String? = null,
isActive: Boolean? = null
) {
if (comment == null && isActive == null) {
_toastLiveData.postValue("변경사항이 없습니다.")
return
}
if (comment != null && comment.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
return
}
_isLoading.value = true
val request = ModifyCommentRequest(commentId = commentId)
if (comment != null) {
request.comment = comment
}
if (isActive != null) {
request.isActive = isActive
}
compositeDisposable.add(
repository.modifyComment(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
page = 1
isLast = false
getCommentList(audioContentId)
} else {
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@@ -1,18 +1,27 @@
package kr.co.vividnext.sodalive.audio_content.comment
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
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.ItemAudioContentCommentBinding
import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentReplyBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentCommentReplyAdapter :
RecyclerView.Adapter<AudioContentCommentReplyViewHolder>() {
class AudioContentCommentReplyAdapter(
private val creatorId: Long,
private val modifyComment: (Long, String) -> Unit,
private val onClickDelete: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentCommentReplyViewHolder>() {
var items = mutableSetOf<GetAudioContentCommentListItem>()
@@ -22,19 +31,25 @@ class AudioContentCommentReplyAdapter :
): AudioContentCommentReplyViewHolder {
return if (viewType == 0) {
AudioContentCommentReplyHeaderViewHolder(
ItemAudioContentCommentBinding.inflate(
binding = ItemAudioContentCommentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
),
)
} else {
AudioContentCommentReplyItemViewHolder(
context = parent.context,
creatorId = creatorId,
ItemAudioContentCommentReplyBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
),
showOptionMenu = { context, view, commentId, writerId, creatorId, onClickModify ->
showOptionMenu(context, view, commentId, writerId, creatorId, onClickModify)
},
modifyComment = modifyComment
)
}
}
@@ -48,6 +63,40 @@ class AudioContentCommentReplyAdapter :
override fun getItemViewType(position: Int): Int {
return position
}
private fun showOptionMenu(
context: Context,
v: View,
commentId: Long,
writerId: Long,
creatorId: Long,
onClickModify: () -> Unit
) {
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
if (writerId == SharedPreferenceManager.userId) {
inflater.inflate(R.menu.content_comment_option_menu, popup.menu)
} else if (creatorId == SharedPreferenceManager.userId) {
inflater.inflate(R.menu.content_comment_option_menu2, popup.menu)
}
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_review_modify -> {
onClickModify()
}
R.id.menu_review_delete -> {
onClickDelete(commentId)
}
}
true
}
popup.show()
}
}
abstract class AudioContentCommentReplyViewHolder(
@@ -63,31 +112,109 @@ 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())
}
val tvCommentLayoutParams = binding.tvComment.layoutParams as LinearLayout.LayoutParams
val can = item.donationCan
if (can > 0) {
tvCommentLayoutParams.topMargin = 0
binding.llDonationCan.visibility = View.VISIBLE
binding.tvDonationCan.text = can.moneyFormat()
binding.llDonationCan.setBackgroundResource(
when {
can >= 100000 -> {
R.drawable.bg_round_corner_10_7_973a3a
}
can >= 50000 -> {
R.drawable.bg_round_corner_10_7_d85e37
}
can >= 10000 -> {
R.drawable.bg_round_corner_10_7_d38c38
}
can >= 5000 -> {
R.drawable.bg_round_corner_10_7_59548f
}
can >= 1000 -> {
R.drawable.bg_round_corner_10_7_4d6aa4
}
can >= 500 -> {
R.drawable.bg_round_corner_10_7_2d7390
}
else -> {
R.drawable.bg_round_corner_10_7_548f7d
}
}
)
} else {
tvCommentLayoutParams.topMargin = 13.3f.dpToPx().toInt()
binding.llDonationCan.visibility = View.GONE
}
binding.tvComment.layoutParams = tvCommentLayoutParams
binding.tvComment.text = item.comment
binding.tvCommentDate.text = item.date
binding.tvCommentNickname.text = item.nickname
binding.tvWriteReply.visibility = View.GONE
binding.ivMenu.visibility = View.GONE
}
}
class AudioContentCommentReplyItemViewHolder(
private val binding: ItemAudioContentCommentReplyBinding
private val context: Context,
private val creatorId: Long,
private val binding: ItemAudioContentCommentReplyBinding,
private val showOptionMenu: (
Context, View, Long, Long, Long, onClickModify: () -> Unit
) -> Unit,
private val modifyComment: (Long, String) -> Unit
) : AudioContentCommentReplyViewHolder(binding) {
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())
}
binding.tvComment.text = item.comment
binding.tvCommentDate.text = item.date
binding.tvCommentNickname.text = item.nickname
if (
item.writerId == SharedPreferenceManager.userId ||
creatorId == SharedPreferenceManager.userId
) {
binding.etCommentModify.setText(item.comment)
binding.ivMenu.visibility = View.VISIBLE
binding.ivMenu.setOnClickListener {
showOptionMenu(
context,
binding.ivMenu,
item.id,
item.writerId,
creatorId
) {
binding.rlCommentModify.visibility = View.VISIBLE
binding.tvComment.visibility = View.GONE
}
}
binding.tvModify.setOnClickListener {
binding.rlCommentModify.visibility = View.GONE
binding.tvComment.visibility = View.VISIBLE
modifyComment(item.id, binding.etCommentModify.text.toString())
}
} else {
binding.ivMenu.visibility = View.GONE
}
}
}

View File

@@ -16,6 +16,7 @@ import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
@@ -34,6 +35,8 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
private lateinit var adapter: AudioContentCommentReplyAdapter
private var originalComment: GetAudioContentCommentListItem? = null
private var creatorId: Long = 0
private var audioContentId: Long = 0
override fun onCreateView(
@@ -41,6 +44,7 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
creatorId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID) ?: 0
audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0
originalComment = BundleCompat.getParcelable(
requireArguments(),
@@ -83,7 +87,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())
}
@@ -94,7 +98,35 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
viewModel.registerComment(audioContentId, originalComment!!.id, comment)
}
adapter = AudioContentCommentReplyAdapter().apply {
adapter = AudioContentCommentReplyAdapter(
creatorId = creatorId,
modifyComment = { commentId, comment ->
hideKeyboard()
viewModel.modifyComment(
commentId = commentId,
parentCommentId = originalComment!!.id,
comment = comment
)
},
onClickDelete = {
SodaDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
title = "댓글 삭제",
desc = "삭제하시겠습니까?",
confirmButtonTitle = "삭제",
confirmButtonClick = {
viewModel.modifyComment(
commentId = it,
parentCommentId = originalComment!!.id,
isActive = false
)
},
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
}
).apply {
items.add(originalComment!!)
}
@@ -189,11 +221,13 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
companion object {
fun newInstance(
creatorId: Long,
audioContentId: Long,
comment: GetAudioContentCommentListItem
): AudioContentCommentReplyFragment {
val args = Bundle()
args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
args.putLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, creatorId)
args.putParcelable(Constants.EXTRA_AUDIO_CONTENT_COMMENT, comment)
val fragment = AudioContentCommentReplyFragment()

View File

@@ -42,11 +42,12 @@ class AudioContentCommentReplyViewModel(
.subscribe(
{
if (it.success && it.data != null) {
page += 1
if (it.data.items.isNotEmpty()) {
page += 1
_commentList.postValue(it.data.items)
} else {
isLast = true
_commentList.postValue(listOf())
}
} else {
if (it.message != null) {
@@ -117,4 +118,61 @@ class AudioContentCommentReplyViewModel(
)
)
}
fun modifyComment(
commentId: Long,
parentCommentId: Long,
comment: String? = null,
isActive: Boolean? = null
) {
if (comment == null && isActive == null) {
_toastLiveData.postValue("변경사항이 없습니다.")
return
}
if (comment != null && comment.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
return
}
_isLoading.value = true
val request = ModifyCommentRequest(commentId = commentId)
if (comment != null) {
request.comment = comment
}
if (isActive != null) {
request.isActive = isActive
}
compositeDisposable.add(
repository.modifyComment(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
page = 1
isLast = false
getCommentReplyList(parentCommentId)
} else {
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@@ -43,4 +43,9 @@ class AudioContentCommentRepository(private val api: AudioContentApi) {
timezone = TimeZone.getDefault().id,
authHeader = token
)
fun modifyComment(request: ModifyCommentRequest, token: String) = api.modifyComment(
request = request,
authHeader = token
)
}

View File

@@ -12,10 +12,11 @@ data class GetAudioContentCommentListResponse(
@Parcelize
data class GetAudioContentCommentListItem(
@SerializedName("id") val id: Long,
@SerializedName("writerId") val writerId: Long,
@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

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.comment
import com.google.gson.annotations.SerializedName
data class ModifyCommentRequest(
@SerializedName("commentId") val commentId: Long,
@SerializedName("comment") var comment: String? = null,
@SerializedName("isActive") var isActive: Boolean? = null
)

View File

@@ -0,0 +1,211 @@
package kr.co.vividnext.sodalive.audio_content.curation
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
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.ActivityAudioContentCurationBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentCurationActivity : BaseActivity<ActivityAudioContentCurationBinding>(
ActivityAudioContentCurationBinding::inflate
) {
private val viewModel: AudioContentCurationViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentNewAllAdapter
private var curationId: Long = 0
private lateinit var title: String
override fun onCreate(savedInstanceState: Bundle?) {
title = intent.getStringExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_TITLE) ?: ""
curationId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_ID, 0)
super.onCreate(savedInstanceState)
if (title.isBlank() || curationId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
bindData()
viewModel.getContentList(curationId = curationId)
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = title
binding.toolbar.tvBack.setOnClickListener { finish() }
adapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - 40f.dpToPx().toInt()) / 2,
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(this, UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvCuration.layoutManager = GridLayoutManager(this, 2)
binding.rvCuration.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
if (position % 2 == 0) {
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
} else {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
}
when (position) {
0, 1 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1, adapter.itemCount - 2 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvCuration.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getContentList(curationId)
}
}
})
binding.rvCuration.adapter = adapter
binding.tvSortNewest.setOnClickListener {
viewModel.changeSort(AudioContentViewModel.Sort.NEWEST)
}
binding.tvSortPriceLow.setOnClickListener {
viewModel.changeSort(AudioContentViewModel.Sort.PRICE_LOW)
}
binding.tvSortPriceHigh.setOnClickListener {
viewModel.changeSort(AudioContentViewModel.Sort.PRICE_HIGH)
}
}
@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.contentListLiveData.observe(this) {
if (viewModel.page - 1 == 1) {
adapter.clear()
binding.rvCuration.scrollToPosition(0)
}
binding.tvTotalCount.text = "${it.totalCount}"
adapter.addItems(it.items)
}
viewModel.sort.observe(this) {
deselectSort()
selectSort(
when (it) {
AudioContentViewModel.Sort.PRICE_HIGH -> {
binding.tvSortPriceHigh
}
AudioContentViewModel.Sort.PRICE_LOW -> {
binding.tvSortPriceLow
}
else -> {
binding.tvSortNewest
}
}
)
viewModel.getContentList(curationId = curationId)
}
}
private fun deselectSort() {
val color = ContextCompat.getColor(
applicationContext,
R.color.color_88e2e2e2
)
binding.tvSortNewest.setTextColor(color)
binding.tvSortPriceLow.setTextColor(color)
binding.tvSortPriceHigh.setTextColor(color)
}
private fun selectSort(view: TextView) {
view.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_e2e2e2
)
)
}
}

View File

@@ -0,0 +1,86 @@
package kr.co.vividnext.sodalive.audio_content.curation
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.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentCurationViewModel(
private val repository: AudioContentRepository
) : 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 _contentListLiveData = MutableLiveData<GetCurationContentResponse>()
val contentListLiveData: LiveData<GetCurationContentResponse>
get() = _contentListLiveData
private val _sort = MutableLiveData(AudioContentViewModel.Sort.NEWEST)
val sort: LiveData<AudioContentViewModel.Sort>
get() = _sort
private var isLast = false
var page = 1
private val size = 10
fun getContentList(curationId: Long) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentListByCurationId(
curationId = curationId,
page = page,
size = size,
sort = _sort.value!!,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
page += 1
_contentListLiveData.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 changeSort(sort: AudioContentViewModel.Sort) {
page = 1
isLast = false
_sort.postValue(sort)
}
}

View File

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.curation
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
data class GetCurationContentResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentMainItem>
)

View File

@@ -40,6 +40,7 @@ import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog
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.report.ReportType
import org.koin.android.ext.android.inject
@@ -56,6 +57,8 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
private var isAlertPreview = false
private val audioContentReceiver = AudioContentReceiver()
private var creatorId: Long = 0
private var refresh = false
set(value) {
field = value
@@ -69,7 +72,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.scrollView.scrollTo(0, 0)
binding.sbProgress.progress = 0
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_play)
binding.ivPlayOrPause.setImageResource(0)
binding.tvTotalDuration.text = " / 00:00:00"
binding.tvCurrentDuration.text = "00:00:00"
binding.rlPreviewAlert.visibility = View.GONE
@@ -102,8 +105,8 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
override fun onPause() {
super.onPause()
unregisterReceiver(audioContentReceiver)
super.onPause()
}
override fun setupView() {
@@ -248,7 +251,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 +263,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() }
}
}
@@ -388,6 +391,14 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
isAlertPreview = it.creator.creatorId != SharedPreferenceManager.userId &&
!it.existOrdered &&
it.price > 0
binding.ivPlayOrPause.setImageResource(
if (isAlertPreview) {
R.drawable.btn_audio_content_preview_play
} else {
R.drawable.btn_audio_content_play
}
)
}
viewModel.isContentPlayLoopLiveData.observe(this) {
@@ -474,7 +485,10 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
private fun showCommentBottomSheetDialog() {
val dialog = AudioContentCommentFragment(audioContentId = audioContentId)
val dialog = AudioContentCommentFragment(
creatorId = creatorId,
audioContentId = audioContentId
)
dialog.show(
supportFragmentManager,
dialog.tag
@@ -491,8 +505,14 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.llPurchase.visibility = View.VISIBLE
binding.tvPrice.text = response.price.toString()
binding.tvStrPurchaseOrRental.text = if (response.isOnlyRental) {
" 대여하기"
} else {
" 구매하기"
}
binding.llPurchase.setOnClickListener {
showOrderDialog(audioContent = response)
showOrderDialog(audioContent = response, isOnlyRental = response.isOnlyRental)
}
} else {
binding.llPurchase.visibility = View.GONE
@@ -629,6 +649,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
private fun setupCreatorArea(creator: AudioContentCreator) {
this.creatorId = creator.creatorId
binding.rlProfile.setOnClickListener {
startActivity(
Intent(applicationContext, UserProfileActivity::class.java).apply {
@@ -670,11 +691,15 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
}
private fun showOrderDialog(audioContent: GetAudioContentDetailResponse) {
private fun showOrderDialog(
audioContent: GetAudioContentDetailResponse,
isOnlyRental: Boolean = false
) {
val dialog = AudioContentOrderFragment(
price = audioContent.price,
onClickKeep = { showOrderConfirmDialog(audioContent, OrderType.KEEP) },
onClickRental = { showOrderConfirmDialog(audioContent, OrderType.RENTAL) }
isOnlyRental = isOnlyRental,
onClickKeep = { showOrderConfirmDialog(audioContent, isOnlyRental, OrderType.KEEP) },
onClickRental = { showOrderConfirmDialog(audioContent, isOnlyRental, OrderType.RENTAL) }
)
dialog.show(
@@ -685,6 +710,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
private fun showOrderConfirmDialog(
audioContent: GetAudioContentDetailResponse,
isOnlyRental: Boolean = false,
orderType: OrderType
) {
AudioContentOrderConfirmDialog(
@@ -693,10 +719,10 @@ 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,
isOnlyRental = isOnlyRental,
orderType = orderType,
price = audioContent.price,
confirmButtonClick = {
@@ -712,7 +738,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
viewModel.order(
contentId = audioContent.contentId,
orderType = orderType
)
) {
val intent = Intent(applicationContext, CanChargeActivity::class.java)
intent.putExtra(Constants.EXTRA_GO_TO_PREV_PAGE, true)
startActivity(intent)
}
},
).show(screenWidth)
}
@@ -752,7 +782,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
if (isPlaying != null && isPlaying) {
R.drawable.btn_audio_content_pause
} else {
R.drawable.btn_audio_content_play
if (isAlertPreview) {
R.drawable.btn_audio_content_preview_play
} else {
R.drawable.btn_audio_content_play
}
}
)
}

View File

@@ -176,7 +176,7 @@ class AudioContentDetailViewModel(
_isShowPreviewAlert.value = !_isShowPreviewAlert.value!!
}
fun order(contentId: Long, orderType: OrderType) {
fun order(contentId: Long, orderType: OrderType, gotoShop: () -> Unit) {
isLoading.value = true
compositeDisposable.add(
repository.orderContent(
@@ -190,10 +190,19 @@ class AudioContentDetailViewModel(
{
if (it.success && it.data != null) {
getAudioContentDetail(audioContentId = contentId)
_toastLiveData.postValue("구매가 완료되었습니다.")
_toastLiveData.postValue(
if (orderType == OrderType.RENTAL) {
"대여가 완료되었습니다."
} else {
"구매가 완료되었습니다."
}
)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
if (it.message.contains("캔이 부족합니다")) {
gotoShop()
}
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
@@ -332,15 +341,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

@@ -16,6 +16,7 @@ data class GetAudioContentDetailResponse(
@SerializedName("duration") val duration: String,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isMosaic") val isMosaic: Boolean,
@SerializedName("isOnlyRental") val isOnlyRental: Boolean,
@SerializedName("existOrdered") val existOrdered: Boolean,
@SerializedName("orderType") val orderType: OrderType?,
@SerializedName("remainingTime") val remainingTime: String?,

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

@@ -14,6 +14,7 @@ import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainCurationAdapter(
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit,
private val onClickCurationMore: (Long, String) -> Unit
) : RecyclerView.Adapter<AudioContentMainCurationAdapter.ViewHolder>() {
private val items = mutableListOf<GetAudioContentCurationResponse>()
@@ -25,6 +26,7 @@ class AudioContentMainCurationAdapter(
fun bind(item: GetAudioContentCurationResponse) {
binding.tvTitle.text = item.title
binding.tvDesc.text = item.description
binding.ivAll.setOnClickListener { onClickCurationMore(item.curationId, item.title) }
setAudioContentList(item.audioContents)
}

View File

@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.app.Service
import android.content.Intent
import android.graphics.Rect
@@ -10,12 +11,17 @@ import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.pointclick.sdk.offerwall.core.PointClickAd
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllActivity
import kr.co.vividnext.sodalive.audio_content.all.AudioContentRankingAllActivity
import kr.co.vividnext.sodalive.audio_content.curation.AudioContentCurationActivity
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.upload.AudioContentUploadActivity
@@ -44,6 +50,8 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
private lateinit var orderListAdapter: AudioContentMainContentAdapter
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentAdapter: AudioContentMainContentAdapter
private lateinit var contentRankingSortAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var contentRankingAdapter: AudioContentMainRankingAdapter
private lateinit var curationAdapter: AudioContentMainCurationAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -80,12 +88,18 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
setupOrderList()
setupNewContentTheme()
setupNewContent()
setupContentRankingSortType()
setupContentRanking()
setupCuration()
binding.swipeRefreshLayout.setOnRefreshListener {
binding.swipeRefreshLayout.isRefreshing = false
viewModel.getMain()
}
binding.ivCanFree.setOnClickListener {
PointClickAd.showOfferwall(requireActivity(), "무료충전")
}
}
private fun setupNewContentCreator() {
@@ -116,7 +130,7 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
outRect.right = 10.7f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
newContentCreatorAdapter.itemCount - 1 -> {
outRect.left = 10.7f.dpToPx().toInt()
outRect.right = 0
}
@@ -142,7 +156,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(
@@ -276,7 +294,7 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
outRect.right = 4f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
newContentThemeAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
@@ -293,6 +311,10 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
}
private fun setupNewContent() {
binding.ivNewContentAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentNewAllActivity::class.java))
}
newContentAdapter = AudioContentMainContentAdapter(
onClickItem = {
startActivity(
@@ -331,7 +353,7 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
outRect.right = 6.7f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
newContentAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
@@ -347,6 +369,88 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
binding.rvNewContent.adapter = newContentAdapter
}
private fun setupContentRankingSortType() {
contentRankingSortAdapter = AudioContentMainNewContentThemeAdapter {
viewModel.getContentRanking(sort = it)
}
binding.rvContentRankingSort.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvContentRankingSort.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)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
contentRankingSortAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvContentRankingSort.adapter = contentRankingSortAdapter
}
private fun setupContentRanking() {
binding.ivContentRankingAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentRankingAllActivity::class.java))
}
contentRankingAdapter = AudioContentMainRankingAdapter(
width = (screenWidth * 0.66).toInt()
) {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
}
binding.rvContentRanking.layoutManager = GridLayoutManager(
context,
3,
GridLayoutManager.HORIZONTAL,
false
)
binding.rvContentRanking.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()
}
})
binding.rvContentRanking.adapter = contentRankingAdapter
}
private fun setupCuration() {
curationAdapter = AudioContentMainCurationAdapter(
onClickItem = {
@@ -362,6 +466,15 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
putExtra(Constants.EXTRA_USER_ID, it)
}
)
},
onClickCurationMore = { curationId, title ->
startActivity(
Intent(requireContext(), AudioContentCurationActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_ID, curationId)
putExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_TITLE, title)
}
)
}
)
@@ -401,6 +514,7 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
binding.rvCuration.adapter = curationAdapter
}
@SuppressLint("SetTextI18n")
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
@@ -466,5 +580,16 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
View.VISIBLE
}
}
viewModel.contentRankingSortListLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
contentRankingSortAdapter.addItems(it)
}
viewModel.contentRankingLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
binding.tvDate.text = "${it.startDate}~${it.endDate}"
contentRankingAdapter.addItems(it.items)
}
}
}

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

@@ -22,7 +22,11 @@ class AudioContentMainNewContentThemeAdapter(
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(theme: String) {
if (theme == selectedTheme || (selectedTheme == "" && theme == "전체")) {
if (
theme == selectedTheme ||
(selectedTheme == "" && theme == "전체") ||
(selectedTheme == "" && theme == "매출")
) {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_9970ff
)

View File

@@ -0,0 +1,65 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
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.ItemAudioContentMainRankingBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainRankingAdapter(
private val width: Int,
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentMainRankingAdapter.AudioContentMainRankingItemViewHolder>() {
inner class AudioContentMainRankingItemViewHolder(
private val binding: ItemAudioContentMainRankingBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(item: GetAudioContentRankingItem, index: Int) {
val lp = binding.root.layoutParams
lp.width = width
binding.root.layoutParams = lp
binding.root.setOnClickListener { onClickItem(item.contentId) }
binding.tvTitle.text = item.title
binding.tvRank.text = "${index + 1}"
binding.tvNickname.text = item.creatorNickname
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(2.7f.dpToPx()))
}
}
}
private val items = mutableListOf<GetAudioContentRankingItem>()
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetAudioContentRankingItem>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = AudioContentMainRankingItemViewHolder(
ItemAudioContentMainRankingBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: AudioContentMainRankingItemViewHolder, position: Int) {
holder.bind(items[position], index = position)
}
}

View File

@@ -45,6 +45,14 @@ class AudioContentMainViewModel(
val curationListLiveData: LiveData<List<GetAudioContentCurationResponse>>
get() = _curationListLiveData
private var _contentRankingSortListLiveData = MutableLiveData<List<String>>()
val contentRankingSortListLiveData: LiveData<List<String>>
get() = _contentRankingSortListLiveData
private var _contentRankingLiveData = MutableLiveData<GetAudioContentRanking>()
val contentRankingLiveData: LiveData<GetAudioContentRanking>
get() = _contentRankingLiveData
fun getMain() {
_isLoading.value = true
compositeDisposable.add(
@@ -61,6 +69,8 @@ class AudioContentMainViewModel(
_orderListLiveData.value = data.orderList
_bannerLiveData.value = data.bannerList
_curationListLiveData.value = data.curationList
_contentRankingLiveData.value = data.contentRanking
_contentRankingSortListLiveData.value = data.contentRankingSortTypeList
val themeList = listOf("전체").union(data.themeList).toList()
_themeListLiveData.value = themeList
@@ -118,4 +128,39 @@ class AudioContentMainViewModel(
)
)
}
fun getContentRanking(sort: String) {
_isLoading.value = true
compositeDisposable.add(
repository.getContentRanking(
page = 1,
size = 12,
sortType = sort,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_isLoading.value = false
_contentRankingLiveData.value = it.data!!
} else {
_isLoading.value = false
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@@ -10,7 +10,9 @@ data class GetAudioContentMainResponse(
@SerializedName("orderList") val orderList: List<GetAudioContentMainItem>,
@SerializedName("themeList") val themeList: List<String>,
@SerializedName("newContentList") val newContentList: List<GetAudioContentMainItem>,
@SerializedName("curationList") val curationList: List<GetAudioContentCurationResponse>
@SerializedName("curationList") val curationList: List<GetAudioContentCurationResponse>,
@SerializedName("contentRankingSortTypeList") val contentRankingSortTypeList: List<String>,
@SerializedName("contentRanking") val contentRanking: GetAudioContentRanking
)
data class GetNewContentUploadCreator(
@@ -23,16 +25,33 @@ data class GetAudioContentMainItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("title") val title: String,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: String
)
data class GetAudioContentRanking(
@SerializedName("startDate") val startDate: String,
@SerializedName("endDate") val endDate: String,
@SerializedName("items") val items: List<GetAudioContentRankingItem>
)
data class GetAudioContentRankingItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("themeStr") val themeStr: String,
@SerializedName("price") val price: Int,
@SerializedName("duration") val duration: String,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String
)
data class GetAudioContentCurationResponse(
@SerializedName("curationId") val curationId: Long,
@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,10 +20,10 @@ class AudioContentOrderConfirmDialog(
title: String,
theme: String,
coverImageUrl: String,
isAdult: Boolean,
profileImageUrl: String,
nickname: String,
duration: String,
isOnlyRental: Boolean,
orderType: OrderType,
price: Int,
confirmButtonClick: () -> Unit,
@@ -48,33 +47,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()}"
dialogView.tvPrice.text = if (orderType == OrderType.RENTAL && !isOnlyRental) {
"${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

@@ -10,6 +10,7 @@ import kotlin.math.ceil
class AudioContentOrderFragment(
private val price: Int,
private val isOnlyRental: Boolean,
private val onClickRental: () -> Unit,
private val onClickKeep: () -> Unit
) : BottomSheetDialogFragment() {
@@ -28,12 +29,18 @@ class AudioContentOrderFragment(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.tvKeep.text = "$price"
binding.tvRental.text = "${ceil(price * 0.7).toInt()}"
if (isOnlyRental) {
binding.tvRental.text = "$price"
binding.rlKeep.visibility = View.GONE
} else {
binding.tvKeep.text = "$price"
binding.tvRental.text = "${ceil(price * 0.6).toInt()}"
binding.llKeep.setOnClickListener {
onClickKeep()
dismiss()
binding.rlKeep.visibility = View.VISIBLE
binding.llKeep.setOnClickListener {
onClickKeep()
dismiss()
}
}
binding.llRental.setOnClickListener {

View File

@@ -81,6 +81,23 @@ class AudioContentOrderListActivity : BaseActivity<ActivityAudioContentOrderList
}
})
binding.rvOrderList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getAudioContentOrderList {}
}
}
})
binding.rvOrderList.adapter = adapter
}
@@ -106,5 +123,9 @@ class AudioContentOrderListActivity : BaseActivity<ActivityAudioContentOrderList
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
viewModel.totalCount.observe(this) {
binding.tvTotalCount.text = "$it"
}
}
}

View File

@@ -23,13 +23,14 @@ 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()))
}
binding.tvTitle.text = item.title
binding.tvTheme.text = item.themeStr
binding.tvDuration.text = item.duration
binding.tvCreatorNickname.text = item.creatorNickname
binding.tvLikeCount.text = item.likeCount.moneyFormat()
binding.tvCommentCount.text = item.commentCount.moneyFormat()

View File

@@ -25,53 +25,60 @@ class AudioContentOrderListViewModel(
val orderList: LiveData<List<GetAudioContentOrderListItem>>
get() = _orderList
private var _totalCount = MutableLiveData<Int>()
val totalCount: LiveData<Int>
get() = _totalCount
private var isLast = false
var page = 1
private val size = 10
fun getAudioContentOrderList(onFailure: (() -> Unit)? = null) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentOrderList(
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
page += 1
_orderList.postValue(it.data.items)
if (_isLoading.value == false) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentOrderList(
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_totalCount.value = it.data.totalCount
if (it.data.items.isNotEmpty()) {
page += 1
_orderList.postValue(it.data.items)
} else {
isLast = true
}
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (onFailure != null) {
onFailure()
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
)
)
)
)
}
}
}

View File

@@ -10,6 +10,7 @@ data class GetAudioContentOrderListResponse(
data class GetAudioContentOrderListItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("title") val title: String,
@SerializedName("themeStr") val themeStr: String,
@SerializedName("duration") val duration: String?,

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
@@ -169,6 +169,8 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
binding.llPricePaid.setOnClickListener { viewModel.setPriceFree(false) }
binding.llPriceFree.setOnClickListener { viewModel.setPriceFree(true) }
binding.llRentalAndKeep.setOnClickListener { viewModel.setIsOnlyRental(false) }
binding.llOnlyRental.setOnClickListener { viewModel.setIsOnlyRental(true) }
binding.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) }
binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) }
@@ -258,6 +260,32 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
}
)
compositeDisposable.add(
binding.etPreviewStartTime.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.isNotBlank()) {
viewModel.previewStartTime = it.toString()
} else {
viewModel.previewStartTime = null
}
}
)
compositeDisposable.add(
binding.etPreviewEndTime.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.isNotBlank()) {
viewModel.previewEndTime = it.toString()
} else {
viewModel.previewEndTime = null
}
}
)
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
@@ -272,51 +300,17 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
viewModel.isPriceFreeLiveData.observe(this) {
if (it) {
viewModel.price = 0
binding.etSetPrice.setText("0")
binding.llSetPrice.visibility = View.GONE
binding.ivPriceFree.visibility = View.VISIBLE
binding.tvPriceFree.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llPriceFree.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivPricePaid.visibility = View.GONE
binding.tvPricePaid.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llPricePaid.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
checkPriceFree()
} else {
binding.llSetPrice.visibility = View.VISIBLE
checkPricePaid()
}
}
binding.ivPricePaid.visibility = View.VISIBLE
binding.tvPricePaid.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llPricePaid.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivPriceFree.visibility = View.GONE
binding.tvPriceFree.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llPriceFree.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
viewModel.isOnlyRentalLiveData.observe(this) {
if (it) {
checkOnlyRental()
} else {
checkRentalAndKeep()
}
}
@@ -414,6 +408,109 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
}
}
private fun checkPriceFree() {
viewModel.price = 0
binding.etSetPrice.setText("0")
binding.llSetPrice.visibility = View.GONE
binding.llConfigKeep.visibility = View.GONE
binding.tvTitleConfigKeep.visibility = View.GONE
binding.ivPriceFree.visibility = View.VISIBLE
binding.tvPriceFree.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llPriceFree.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivPricePaid.visibility = View.GONE
binding.tvPricePaid.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llPricePaid.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
binding.llConfigPreviewTime.visibility = View.GONE
}
private fun checkPricePaid() {
binding.llSetPrice.visibility = View.VISIBLE
binding.llConfigKeep.visibility = View.VISIBLE
binding.tvTitleConfigKeep.visibility = View.VISIBLE
binding.ivPricePaid.visibility = View.VISIBLE
binding.tvPricePaid.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llPricePaid.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivPriceFree.visibility = View.GONE
binding.tvPriceFree.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llPriceFree.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
binding.llConfigPreviewTime.visibility = View.VISIBLE
}
private fun checkRentalAndKeep() {
binding.tvPriceTitle.text = "소장 가격"
binding.ivRentalAndKeep.visibility = View.VISIBLE
binding.tvRentalAndKeep.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llRentalAndKeep.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivOnlyRental.visibility = View.GONE
binding.tvOnlyRental.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llOnlyRental.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
}
private fun checkOnlyRental() {
binding.tvPriceTitle.text = "대여 가격"
binding.ivOnlyRental.visibility = View.VISIBLE
binding.tvOnlyRental.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llOnlyRental.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivRentalAndKeep.visibility = View.GONE
binding.tvRentalAndKeep.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llRentalAndKeep.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
}
private fun getFileName(uri: Uri): String? {
val scheme = uri.scheme
var fileName: String? = null

View File

@@ -18,6 +18,8 @@ import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
class AudioContentUploadViewModel(
private val repository: AudioContentRepository
@@ -35,6 +37,10 @@ class AudioContentUploadViewModel(
val isAdultLiveData: LiveData<Boolean>
get() = _isAdultLiveData
private val _isOnlyRentalLiveData = MutableLiveData(false)
val isOnlyRentalLiveData: LiveData<Boolean>
get() = _isOnlyRentalLiveData
private val _isAvailableCommentLiveData = MutableLiveData(true)
val isAvailableCommentLiveData: LiveData<Boolean>
get() = _isAvailableCommentLiveData
@@ -52,6 +58,8 @@ class AudioContentUploadViewModel(
var theme: GetAudioContentThemeResponse? = null
var coverImageUri: Uri? = null
var contentUri: Uri? = null
var previewStartTime: String? = null
var previewEndTime: String? = null
fun setAdult(isAdult: Boolean) {
_isAdultLiveData.postValue(isAdult)
@@ -63,6 +71,14 @@ class AudioContentUploadViewModel(
fun setPriceFree(isPriceFree: Boolean) {
_isPriceFreeLiveData.postValue(isPriceFree)
if (isPriceFree) {
_isOnlyRentalLiveData.postValue(false)
}
}
fun setIsOnlyRental(isOnlyRental: Boolean) {
_isOnlyRentalLiveData.postValue(isOnlyRental)
}
fun uploadAudioContent(onSuccess: () -> Unit) {
@@ -76,7 +92,10 @@ class AudioContentUploadViewModel(
price = price,
themeId = theme!!.id,
isAdult = _isAdultLiveData.value!!,
isCommentAvailable = _isAvailableCommentLiveData.value!!
isOnlyRental = _isOnlyRentalLiveData.value!!,
isCommentAvailable = _isAvailableCommentLiveData.value!!,
previewStartTime = previewStartTime,
previewEndTime = previewEndTime
)
val requestJson = Gson().toJson(request)
@@ -206,16 +225,86 @@ class AudioContentUploadViewModel(
return false
}
if (previewStartTime != null && previewEndTime != null) {
val startTimeArray = previewStartTime!!.split(":")
if (startTimeArray.size != 3) {
_toastLiveData.postValue("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
return false
}
for (time in startTimeArray) {
if (time.length != 2) {
_toastLiveData.postValue("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
return false
}
}
val endTimeArray = previewEndTime!!.split(":")
if (endTimeArray.size != 3) {
_toastLiveData.postValue("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
return false
}
for (time in endTimeArray) {
if (time.length != 2) {
_toastLiveData.postValue("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
return false
}
}
val timeDifference = timeDifference(previewStartTime!!, previewEndTime!!)
if (timeDifference < 30000) {
_toastLiveData.postValue(
"미리 듣기의 최소 시간은 30초 입니다."
)
return false
}
} else {
if (previewStartTime != null || previewEndTime != null) {
_toastLiveData.postValue(
"미리 듣기 시작 시간과 종료 시간 둘 다 입력을 하거나 둘 다 입력 하지 않아야 합니다."
)
return false
}
}
if (contentUri == null) {
_toastLiveData.postValue("오디오 콘텐츠를 선택해 주세요.")
return false
}
if (!isPriceFreeLiveData.value!! && price < 10) {
_toastLiveData.postValue("콘텐츠의 최소금액은 10코인 입니다.")
if (!isPriceFreeLiveData.value!! && price < 5) {
_toastLiveData.postValue("콘텐츠의 최소금액은 5캔 입니다.")
return false
}
return true
}
private fun timeDifference(startTime: String, endTime: String): Long {
try {
// Define a date format for parsing the times
val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.KOREAN)
// Parse the input times into Date objects
val date1 = dateFormat.parse(startTime)
val date2 = dateFormat.parse(endTime)
// Check if either date is null
if (date1 == null || date2 == null) {
return 0
}
// Calculate the absolute time difference in milliseconds
// Check if the time difference is greater than 30 seconds (30000 milliseconds)
return date2.time - date1.time
} catch (e: Exception) {
// Handle invalid time formats or parsing errors
return 0
}
}
}

View File

@@ -9,5 +9,8 @@ data class CreateAudioContentRequest(
@SerializedName("price") val price: Int,
@SerializedName("themeId") val themeId: Long,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isOnlyRental") val isOnlyRental: Boolean,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean,
@SerializedName("previewStartTime") val previewStartTime: String? = null,
@SerializedName("previewEndTime") val previewEndTime: String? = null
)

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

@@ -8,11 +8,13 @@ object Constants {
const val PREF_IS_ADULT = "pref_is_adult"
const val PREF_NICKNAME = "pref_nickname"
const val PREF_USER_ROLE = "pref_user_role"
const val PREF_NO_CHAT_ROOM = "pref_no_chat"
const val PREF_PUSH_TOKEN = "pref_push_token"
const val PREF_PROFILE_IMAGE = "pref_profile_image"
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"
@@ -27,7 +29,7 @@ object Constants {
const val EXTRA_MESSAGE_BOX = "extra_message_box"
const val EXTRA_TEXT_MESSAGE = "extra_text_message"
const val EXTRA_LIVE_TIME_NOW = "extra_live_time_now"
const val EXTRA_PREV_LIVE_ROOM = "extra_prev_live_room"
const val EXTRA_GO_TO_PREV_PAGE = "extra_go_to_prev_page"
const val EXTRA_SELECT_RECIPIENT = "extra_select_recipient"
const val EXTRA_ROOM_CHANNEL_NAME = "extra_room_channel_name"
const val EXTRA_LIVE_RESERVATION_RESPONSE = "extra_live_reservation_response"
@@ -45,6 +47,8 @@ object Constants {
const val EXTRA_AUDIO_CONTENT_COMMENT = "audio_content_comment"
const val EXTRA_AUDIO_CONTENT_LOADING = "audio_content_loading"
const val EXTRA_AUDIO_CONTENT_CREATOR_ID = "audio_content_creator_id"
const val EXTRA_AUDIO_CONTENT_CURATION_ID = "extra_audio_content_curation_id"
const val EXTRA_AUDIO_CONTENT_CURATION_TITLE = "extra_audio_content_curation_title"
const val EXTRA_AUDIO_CONTENT_NEXT_ACTION = "audio_content_next_action"
const val EXTRA_AUDIO_CONTENT_ALERT_PREVIEW = "audio_content_alert_preview"
const val EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL = "audio_content_cover_image_url"

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

@@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.common
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kr.co.vividnext.sodalive.settings.notification.MemberRole
object SharedPreferenceManager {
@@ -116,4 +118,24 @@ 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
}
var noChatRoomList: List<Long>
get() {
val list = sharedPreferences[Constants.PREF_NO_CHAT_ROOM, ""]
val gson = Gson()
val listType = object : TypeToken<List<Long>>() {}.type
val myList = gson.fromJson<List<Long>>(list, listType)
return myList ?: emptyList()
}
set(value) {
val gson = Gson()
val listJson = gson.toJson(value)
sharedPreferences[Constants.PREF_NO_CHAT_ROOM] = listJson
}
}

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

@@ -7,9 +7,12 @@ import kr.co.vividnext.sodalive.audio_content.AudioContentApi
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
import kr.co.vividnext.sodalive.audio_content.PlaybackTrackingRepository
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllViewModel
import kr.co.vividnext.sodalive.audio_content.all.AudioContentRankingAllViewModel
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentListViewModel
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentReplyViewModel
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentRepository
import kr.co.vividnext.sodalive.audio_content.curation.AudioContentCurationViewModel
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailViewModel
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainViewModel
import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyViewModel
@@ -22,6 +25,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 +60,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 +144,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 +190,13 @@ 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(), get()) }
viewModel { AudioContentCurationViewModel(get()) }
viewModel { AudioContentNewAllViewModel(get()) }
viewModel { AudioContentRankingAllViewModel(get()) }
}
private val repositoryModule = module {
@@ -199,6 +217,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

@@ -27,6 +27,7 @@ class ExplorerAdapter(
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetExplorerSectionResponse) {
setTitle(item)
setDesc(item)
setCreatorList(item)
}
@@ -56,8 +57,20 @@ class ExplorerAdapter(
}
}
private fun setDesc(item: GetExplorerSectionResponse) {
if (item.desc != null) {
binding.llDesc.visibility = View.VISIBLE
binding.tvDesc.text = item.desc
} else {
binding.llDesc.visibility = View.GONE
}
}
private fun setCreatorList(item: GetExplorerSectionResponse) {
val adapter = ExplorerSectionAdapter(onClickItem = onClickItem)
val adapter = ExplorerSectionAdapter(
onClickItem = onClickItem,
isVisibleRanking = item.desc != null
)
binding.rvExplorerSection.layoutManager = LinearLayoutManager(
context,

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

@@ -85,17 +85,17 @@ class ExplorerFragment : BaseFragment<FragmentExplorerBinding>(
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 0
outRect.bottom = 30f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 30f.dpToPx().toInt()
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 0
}
else -> {
outRect.top = 30f.dpToPx().toInt()
outRect.bottom = 30f.dpToPx().toInt()
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
}
}

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,
@@ -36,14 +56,10 @@ class ExplorerRepository(
)
fun modifyCheers(
cheersId: Long,
content: String,
request: PutModifyCheersRequest,
token: String
) = api.modifyCheers(
request = PutModifyCheersRequest(
cheersId = cheersId,
content = content
),
request = request,
authHeader = token
)
@@ -63,4 +79,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

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.explorer
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
@@ -10,7 +11,8 @@ import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemExplorerSectionBinding
class ExplorerSectionAdapter(
private val onClickItem: (Long) -> Unit
private val onClickItem: (Long) -> Unit,
private val isVisibleRanking: Boolean
) : RecyclerView.Adapter<ExplorerSectionAdapter.ViewHolder>() {
private val items = mutableListOf<GetExplorerSectionCreatorResponse>()
@@ -18,17 +20,50 @@ class ExplorerSectionAdapter(
inner class ViewHolder(
private val binding: ItemExplorerSectionBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetExplorerSectionCreatorResponse) {
fun bind(item: GetExplorerSectionCreatorResponse, index: Int) {
binding.root.setOnClickListener { onClickItem(item.id) }
binding.tvNickname.text = item.nickname
binding.tvTags.text = item.tags
binding.ivProfile.load(item.profileImageUrl) {
transformations(CircleCropTransformation())
placeholder(R.drawable.ic_logo)
placeholder(R.drawable.ic_place_holder)
crossfade(true)
}
binding.root.setOnClickListener { onClickItem(item.id) }
if (isVisibleRanking) {
when (index) {
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
}
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
}
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
}
else -> {
binding.ivBg.setImageResource(0)
binding.ivBg.visibility = View.GONE
binding.ivCrown.visibility = View.GONE
}
}
}
}
}
@@ -41,7 +76,7 @@ class ExplorerSectionAdapter(
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
holder.bind(items[position], index = position)
}
override fun getItemCount() = items.size

View File

@@ -10,6 +10,7 @@ data class GetExplorerSectionResponse(
@SerializedName("title") val title: String,
@SerializedName("coloredTitle") val coloredTitle: String?,
@SerializedName("color") val color: String?,
@SerializedName("desc") val desc: String?,
@SerializedName("creators") val creators: List<GetExplorerSectionCreatorResponse>
)

View File

@@ -9,6 +9,7 @@ data class GetCheersResponse(
data class GetCheersResponseItem(
@SerializedName("cheersId") val cheersId: Long,
@SerializedName("memberId") val memberId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("content") val content: String,

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,7 +23,12 @@ 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.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
@@ -56,6 +61,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 +124,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
setupDonationView()
setupSimilarCreatorView()
setupFanTalkView()
setupAudioContentListView()
}
private fun hideKeyboard(onAfterExecute: () -> Unit) {
@@ -392,7 +399,34 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
)
}
},
onClickReport = { showCheersReportPopup(it) }
modifyCheers = { cheersId, content ->
hideKeyboard {
viewModel.modifyCheers(
cheersId = cheersId,
creatorId = userId,
cheersContent = content,
)
}
},
onClickReport = { showCheersReportPopup(it) },
onClickDelete = {
SodaDialog(
activity = this@UserProfileActivity,
layoutInflater = layoutInflater,
title = "응원글 삭제",
desc = "삭제하시겠습니까?",
confirmButtonTitle = "삭제",
confirmButtonClick = {
viewModel.modifyCheers(
cheersId = it,
creatorId = userId,
isActive = false
)
},
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
}
)
rvCheers.layoutManager = LinearLayoutManager(
@@ -432,6 +466,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 +557,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 +617,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 +706,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

@@ -15,6 +15,7 @@ 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.cheers.PutModifyCheersRequest
import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.report.ReportRequest
import kr.co.vividnext.sodalive.report.ReportType
@@ -256,18 +257,37 @@ class UserProfileViewModel(
)
}
fun modifyCheers(cheersId: Long, creatorId: Long, cheersContent: String) {
if (cheersContent.isBlank()) {
fun modifyCheers(
cheersId: Long,
creatorId: Long,
cheersContent: String? = null,
isActive: Boolean? = null
) {
if (cheersContent == null && isActive == null) {
_toastLiveData.postValue("변경사항이 없습니다.")
return
}
if (cheersContent != null && cheersContent.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
return
}
_isLoading.value = true
val request = PutModifyCheersRequest(cheersId = cheersId)
if (cheersContent != null) {
request.content = cheersContent
}
if (isActive != null) {
request.isActive = isActive
}
compositeDisposable.add(
repository.modifyCheers(
cheersId = cheersId,
content = cheersContent,
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
@@ -295,16 +315,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

@@ -4,5 +4,6 @@ import com.google.gson.annotations.SerializedName
data class PutModifyCheersRequest(
@SerializedName("cheersId") val cheersId: Long,
@SerializedName("content") val content: String
@SerializedName("content") var content: String? = null,
@SerializedName("isActive") var isActive: Boolean? = null,
)

View File

@@ -17,8 +17,10 @@ import kr.co.vividnext.sodalive.extensions.dpToPx
class UserProfileCheersAdapter(
private val userId: Long,
private val enterReply: (Long, String) -> Unit,
private val modifyReply: (Long, String) -> Unit,
private val onClickReport: (Long) -> Unit
private val modifyReply: (Long, String?) -> Unit,
private val modifyCheers: (Long, String) -> Unit,
private val onClickReport: (Long) -> Unit,
private val onClickDelete: (Long) -> Unit
) : RecyclerView.Adapter<UserProfileCheersAdapter.ViewHolder>() {
val items = mutableListOf<GetCheersResponseItem>()
@@ -35,19 +37,40 @@ 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
binding.tvNickname.text = cheers.nickname
binding.tvDate.text = cheers.date
binding.ivMenu.setOnClickListener {
showOptionMenu(
context,
binding.ivMenu,
cheersId = cheers.cheersId
)
if (
cheers.memberId == SharedPreferenceManager.userId ||
userId == SharedPreferenceManager.userId
) {
binding.etContentModify.setText(cheers.content)
binding.ivMenu.visibility = View.VISIBLE
binding.ivMenu.setOnClickListener {
showOptionMenu(
context,
binding.ivMenu,
cheersId = cheers.cheersId,
memberId = cheers.memberId,
creatorId = userId,
onClickModify = {
binding.rlContentModify.visibility = View.VISIBLE
binding.tvContent.visibility = View.GONE
}
)
}
binding.tvModify.setOnClickListener {
binding.rlContentModify.visibility = View.GONE
binding.tvContent.visibility = View.VISIBLE
modifyCheers(cheers.cheersId, binding.etContentModify.text.toString())
}
} else {
binding.ivMenu.visibility = View.GONE
}
if (cheers.replyList.isNotEmpty()) {
@@ -106,16 +129,38 @@ class UserProfileCheersAdapter(
override fun getItemCount() = items.count()
private fun showOptionMenu(context: Context, v: View, cheersId: Long) {
private fun showOptionMenu(
context: Context,
v: View,
cheersId: Long,
memberId: Long,
creatorId: Long,
onClickModify: () -> Unit
) {
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
inflater.inflate(R.menu.review_option_menu, popup.menu)
if (memberId == SharedPreferenceManager.userId) {
inflater.inflate(R.menu.review_option_menu3, popup.menu)
} else if (creatorId == SharedPreferenceManager.userId) {
inflater.inflate(R.menu.review_option_menu2, popup.menu)
} else {
inflater.inflate(R.menu.review_option_menu, popup.menu)
}
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_review_report -> {
onClickReport(cheersId)
}
R.id.menu_review_modify -> {
onClickModify()
}
R.id.menu_review_delete -> {
onClickDelete(cheersId)
}
}
true

View File

@@ -0,0 +1,19 @@
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("isVisibleDonationRank")
val isVisibleDonationRank: Boolean,
@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_13181b
} else {
R.drawable.bg_top_round_corner_4_7_13181b
}
)
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_13181b
)
} else {
binding.rlDonationRankingRoot.setBackgroundColor(
ContextCompat.getColor(context, R.color.color_13181b)
)
}
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_13181b
)
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,165 @@
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.R
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
binding.llVisibleDonationRanking.visibility = View.VISIBLE
binding.ivVisibleDonationRank.setOnClickListener {
viewModel.onClickToggleVisibleDonationRank()
}
} else {
binding.llTotal.visibility = View.GONE
binding.llVisibleDonationRanking.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()
}
viewModel.isVisibleDonationRank.observe(this) {
binding.ivVisibleDonationRank.setImageResource(
if (it) {
R.drawable.btn_toggle_on_big
} else {
R.drawable.btn_toggle_off_big
}
)
}
}
}

View File

@@ -0,0 +1,128 @@
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
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateRequest
import kr.co.vividnext.sodalive.user.UserRepository
class UserProfileDonationAllViewModel(
private val repository: ExplorerRepository,
private val memberRepository: UserRepository
) : 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 _isVisibleDonationRank = MutableLiveData<Boolean>()
val isVisibleDonationRank: LiveData<Boolean>
get() = _isVisibleDonationRank
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
}
_isVisibleDonationRank.postValue(it.data.isVisibleDonationRank)
} 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)
}
fun onClickToggleVisibleDonationRank() {
val nowStateVisibleDonationRank = _isVisibleDonationRank.value!!
_isLoading.value = true
compositeDisposable.add(
memberRepository.updateProfile(
request = ProfileUpdateRequest(
email = SharedPreferenceManager.email,
isVisibleDonationRank = !nowStateVisibleDonationRank
),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
_isVisibleDonationRank.postValue(!nowStateVisibleDonationRank)
} 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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@@ -1,10 +1,231 @@
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.base.SodaDialog
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
)
}
},
modifyCheers = { cheersId, content ->
hideKeyboard {
viewModel.modifyCheers(
cheersId = cheersId,
creatorId = userId,
cheersContent = content,
)
}
},
onClickReport = { showCheersReportPopup(it) },
onClickDelete = {
SodaDialog(
activity = this@UserProfileFantalkAllViewActivity,
layoutInflater = layoutInflater,
title = "응원글 삭제",
desc = "삭제하시겠습니까?",
confirmButtonTitle = "삭제",
confirmButtonClick = {
viewModel.modifyCheers(
cheersId = it,
creatorId = userId,
isActive = false
)
},
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
}
)
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,206 @@
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.explorer.profile.cheers.PutModifyCheersRequest
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? = null,
isActive: Boolean? = null
) {
if (cheersContent == null && isActive == null) {
_toastLiveData.postValue("변경사항이 없습니다.")
return
}
if (cheersContent != null && cheersContent.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
return
}
_isLoading.value = true
val request = PutModifyCheersRequest(cheersId = cheersId)
if (cheersContent != null) {
request.content = cheersContent
}
if (isActive != null) {
request.isActive = isActive
}
compositeDisposable.add(
repository.modifyCheers(
request = request,
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

@@ -1,13 +1,18 @@
package kr.co.vividnext.sodalive.live.reservation
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
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 com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ItemLiveReservationBinding
import kr.co.vividnext.sodalive.databinding.ItemMyLiveReservationBinding
@@ -24,6 +29,7 @@ class LiveReservationAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == 1) {
MyLiveViewHolder(
parent.context,
ItemMyLiveReservationBinding.inflate(
LayoutInflater.from(parent.context),
parent,
@@ -32,6 +38,7 @@ class LiveReservationAdapter(
)
} else {
ViewHolder(
parent.context,
ItemLiveReservationBinding.inflate(
LayoutInflater.from(parent.context),
parent,
@@ -61,7 +68,7 @@ class LiveReservationAdapter(
}
private fun isMyLive(item: GetRoomListResponse) =
item.managerId == SharedPreferenceManager.userId && isMain
item.creatorId == SharedPreferenceManager.userId && isMain
@SuppressLint("NotifyDataSetChanged")
fun clear() {
@@ -70,17 +77,34 @@ class LiveReservationAdapter(
}
inner class ViewHolder(
private val context: Context,
private val binding: ItemLiveReservationBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetRoomListResponse) {
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
}
val lp = binding.ivCover.layoutParams
lp.width = 80f.dpToPx().toInt()
lp.height = 116.7f.dpToPx().toInt()
Glide
.with(context)
.asBitmap()
.transform(CenterCrop(), RoundedCorners(16f.dpToPx().toInt()))
.load(item.coverImageUrl)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
binding.ivCover.setImageBitmap(resource)
binding.ivCover.layoutParams = lp
}
override fun onLoadCleared(placeholder: Drawable?) {
}
})
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,16 +126,11 @@ class LiveReservationAdapter(
"${item.price.moneyFormat()}"
}
}
binding.iv19.visibility = if (item.isAdult) {
View.VISIBLE
} else {
View.GONE
}
}
}
inner class MyLiveViewHolder(
private val context: Context,
private val binding: ItemMyLiveReservationBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetRoomListResponse, position: Int) {
@@ -120,13 +139,30 @@ class LiveReservationAdapter(
} else {
View.GONE
}
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(RoundedCornersTransformation(4f.dpToPx()))
}
val lp = binding.ivCover.layoutParams
lp.width = 80f.dpToPx().toInt()
lp.height = 116.7f.dpToPx().toInt()
Glide
.with(context)
.asBitmap()
.load(item.coverImageUrl)
.transform(CenterCrop(), RoundedCorners(16f.dpToPx().toInt()))
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
binding.ivCover.setImageBitmap(resource)
binding.ivCover.layoutParams = lp
}
override fun onLoadCleared(placeholder: Drawable?) {
}
})
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 +171,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

@@ -1,15 +1,12 @@
package kr.co.vividnext.sodalive.live.reservation.all
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -30,7 +27,6 @@ import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.reservation.LiveReservationAdapter
import kr.co.vividnext.sodalive.live.reservation.complete.LiveReservationCompleteActivity
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment
import kr.co.vividnext.sodalive.live.room.dialog.LiveCancelDialog
@@ -51,19 +47,9 @@ class LiveReservationAllActivity : BaseActivity<ActivityLiveReservationAllBindin
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: LiveReservationAdapter
private lateinit var selectedDateString: String
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
refresh()
}
}
setupCalendar()
}
@@ -198,10 +184,6 @@ class LiveReservationAllActivity : BaseActivity<ActivityLiveReservationAllBindin
if (adapter.items.isEmpty()) {
binding.swipeRefreshLayout.visibility = View.GONE
binding.llNoItems.visibility = View.VISIBLE
binding.llNoItems.setOnClickListener {
val intent = Intent(applicationContext, LiveRoomCreateActivity::class.java)
activityResultLauncher.launch(intent)
}
}
} else {
binding.swipeRefreshLayout.visibility = View.VISIBLE

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

@@ -11,6 +11,7 @@ import android.graphics.Rect
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
import android.os.Handler
import android.os.Looper
import android.text.Spannable
@@ -106,7 +107,57 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
private var isMicrophoneMute = false
private var isSpeaker = false
private var isSpeakerFold = false
private var isAvailableDonation = false
private var isNoChatting = false
private var remainingNoChattingTime = noChattingTime
private val countDownTimer = object : CountDownTimer(remainingNoChattingTime * 1000, 1000) {
override fun onTick(millisUntilFinished: Long) {
remainingNoChattingTime -= 1
}
override fun onFinish() {
isNoChatting = false
remainingNoChattingTime = noChattingTime
removeNoChatRoom()
Toast.makeText(
applicationContext,
"채팅금지가 해제되었습니다.",
Toast.LENGTH_SHORT
).show()
}
}
private fun startNoChatting() {
hideKeyboard {
binding.etChat.clearFocus()
isNoChatting = true
Toast.makeText(
applicationContext,
"${viewModel.getManagerNickname()}님이 3분간 채팅을 금지하였습니다.",
Toast.LENGTH_SHORT
).show()
countDownTimer.start()
}
}
private fun addNoChatRoom() {
val noChatRoomList = SharedPreferenceManager.noChatRoomList.toMutableList()
noChatRoomList.add(roomId)
SharedPreferenceManager.noChatRoomList = noChatRoomList
}
private fun removeNoChatRoom() {
val noChatRoomList = SharedPreferenceManager.noChatRoomList.toMutableList()
noChatRoomList.remove(roomId)
SharedPreferenceManager.noChatRoomList = noChatRoomList
}
private fun containNoChatRoom(): Boolean {
val noChatRoomList = SharedPreferenceManager.noChatRoomList
return noChatRoomList.contains(roomId)
}
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
@@ -151,6 +202,17 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
viewModel.getMemberCan()
viewModel.getRoomInfo(roomId)
binding.etChat.setOnFocusChangeListener { view, hasFocus ->
if (isNoChatting && hasFocus) {
Toast.makeText(
applicationContext,
"${remainingNoChattingTime}초 동안 채팅하실 수 없습니다",
Toast.LENGTH_SHORT
).show()
view.clearFocus()
}
}
}
override fun onStart() {
@@ -210,7 +272,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
)
},
onClickInviteSpeaker = { memberId ->
if (speakerListAdapter.itemCount <= 9) {
if (speakerListAdapter.itemCount <= 4) {
inviteSpeaker(memberId)
} else {
showToast("스피커 정원이 초과했습니다.")
@@ -233,6 +295,15 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
changeListenerMessage(memberId)
},
onClickNoChatting = { userId, nickname, profileUrl ->
LiveRoomNoChattingDialog(
activity = this,
layoutInflater = layoutInflater,
nickname = nickname,
profileUrl = profileUrl,
confirmButtonClick = { setNoChatting(userId, nickname) }
).show(screenWidth)
},
onClickKickOut = {
LiveDialog(
activity = this,
@@ -286,7 +357,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
)
},
onClickInviteSpeaker = {
if (speakerListAdapter.itemCount <= 9) {
if (speakerListAdapter.itemCount <= 4) {
inviteSpeaker(it)
} else {
showToast("스피커 정원이 초과했습니다.")
@@ -305,6 +376,15 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
cancelButtonClick = {}
).show(screenWidth)
},
onClickNoChatting = { userId, nickname, profileUrl ->
LiveRoomNoChattingDialog(
activity = this,
layoutInflater = layoutInflater,
nickname = nickname,
profileUrl = profileUrl,
confirmButtonClick = { setNoChatting(userId, nickname) }
).show(screenWidth)
},
onClickPopupMenu = { userId, nickname, isBlock, view ->
showOptionMenu(
this,
@@ -399,6 +479,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
agora.deInitAgoraEngine()
}
}
countDownTimer.cancel()
super.onDestroy()
}
@@ -559,21 +640,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 +692,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 +712,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)
}
}
@@ -672,31 +759,36 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
}
binding.llViewUsers.setOnClickListener { roomProfileDialog.show() }
if (response.creatorId == SharedPreferenceManager.userId) {
binding.llViewUsers.visibility = View.VISIBLE
binding.llViewUsers.setOnClickListener { roomProfileDialog.show() }
} else {
binding.llViewUsers.visibility = View.GONE
}
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 +796,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
)
}
@@ -963,6 +1055,17 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
}
private fun setNoChatting(userId: Long, nickname: String) {
agora.sendRawMessageToPeer(
receiverUid = userId.toString(),
requestType = LiveRoomRequestType.NO_CHATTING
) {
handler.post {
showDialog(content = "${nickname}님을 3분간 채팅금지를 하였습니다.")
}
}
}
private fun showDialog(
content: String,
cancelTitle: String = "",
@@ -1004,7 +1107,13 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
val profileUrl = viewModel.getUserProfileUrl(SharedPreferenceManager.userId.toInt())
val rank = viewModel.getUserRank(SharedPreferenceManager.userId)
if (binding.etChat.text.isNotBlank() && nickname.isNotBlank() && profileUrl.isNotBlank()) {
if (isNoChatting) {
Toast.makeText(
applicationContext,
"${remainingNoChattingTime}초 동안 채팅하실 수 없습니다",
Toast.LENGTH_SHORT
).show()
} else if (binding.etChat.text.isNotBlank() && nickname.isNotBlank() && profileUrl.isNotBlank()) {
val message = binding.etChat.text.toString()
chatAdapter.items.add(
LiveRoomNormalChat(
@@ -1082,6 +1191,8 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
private fun joinChannel(roomInfo: GetRoomInfoResponse) {
loadingDialog.show(width = screenWidth, message = "라이브에 입장하고 있습니다.")
val userId = SharedPreferenceManager.userId
agora.joinRtcChannel(
uid = userId.toInt(),
@@ -1190,7 +1301,11 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
},
rtmChannelJoinSuccess = {
if (userId == roomInfo.managerId) {
handler.post {
loadingDialog.dismiss()
}
if (userId == roomInfo.creatorId) {
setBroadcaster()
} else {
setAudience()
@@ -1204,6 +1319,10 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} else {
startService(intent)
}
if (containNoChatRoom()) {
startNoChatting()
}
},
rtmChannelJoinFail = {
agoraConnectFail()
@@ -1212,8 +1331,11 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
private fun agoraConnectFail() {
showToast("라이브에 접속하지 못했습니다.\n다시 시도해 주세요.")
finish()
handler.post {
loadingDialog.dismiss()
showToast("라이브에 접속하지 못했습니다.\n다시 시도해 주세요.")
finish()
}
}
private val rtcEventHandler = object : IRtcEngineEventHandler() {
@@ -1398,7 +1520,19 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
viewModel.getRoomInfo(roomId = roomId)
return
}
if (rawMessage == LiveRoomRequestType.NO_CHATTING.toString() && !isNoChatting) {
handler.post {
addNoChatRoom()
startNoChatting()
}
return
}
}
}
}
companion object {
private const val noChattingTime = 180L
}
}

View File

@@ -0,0 +1,62 @@
package kr.co.vividnext.sodalive.live.room
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.DialogLiveNoChattingBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class LiveRoomNoChattingDialog(
activity: Activity,
layoutInflater: LayoutInflater,
nickname: String,
profileUrl: String,
confirmButtonClick: () -> Unit,
) {
private val alertDialog: AlertDialog
val dialogView = DialogLiveNoChattingBinding.inflate(layoutInflater)
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialogView.tvCancel.setOnClickListener {
alertDialog.dismiss()
}
dialogView.tvConfirm.setOnClickListener {
alertDialog.dismiss()
confirmButtonClick()
}
dialogView.tvNickname.text = nickname
dialogView.ivProfile.load(profileUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
}
fun show(width: Int) {
alertDialog.show()
val lp = WindowManager.LayoutParams()
lp.copyFrom(alertDialog.window?.attributes)
lp.width = width - (26.7f.dpToPx()).toInt()
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
alertDialog.window?.attributes = lp
}
}

View File

@@ -8,4 +8,5 @@ enum class LiveRoomRequestType {
@SerializedName("KICK_OUT") KICK_OUT,
@SerializedName("SET_MANAGER") SET_MANAGER,
@SerializedName("RELEASE_MANAGER") RELEASE_MANAGER,
@SerializedName("NO_CHATTING") NO_CHATTING,
}

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,9 @@ 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
viewModel.getDetail(roomId) { dismiss() }
}
private fun bindData() {
@@ -140,28 +83,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 +111,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 +158,6 @@ class LiveRoomDetailFragment(
}
}
} else {
binding.tvParticipateExpression.text = "참여자"
binding.tvReservationComplete.visibility = View.GONE
binding.tvParticipateNow.visibility = View.VISIBLE
binding.tvReservation.visibility = View.GONE
@@ -226,9 +167,6 @@ class LiveRoomDetailFragment(
}
binding.llStartDelete.visibility = View.GONE
}
adapter.items.addAll(response.participatingUsers)
adapter.notifyDataSetChanged()
}
private fun setManagerProfile(manager: GetRoomDetailManager) {
@@ -236,7 +174,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 +228,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 = "입장하기"
}

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