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 'org.jlleitschuh.gradle.ktlint'
id 'io.objectbox' id 'io.objectbox'
id("com.google.firebase.crashlytics") id 'com.google.firebase.crashlytics'
} }
android { android {
@@ -26,6 +26,7 @@ android {
lintOptions { lintOptions {
checkDependencies true checkDependencies true
checkReleaseBuilds false
} }
dependenciesInfo { dependenciesInfo {
@@ -39,8 +40,8 @@ android {
applicationId "kr.co.vividnext.sodalive" applicationId "kr.co.vividnext.sodalive"
minSdk 23 minSdk 23
targetSdk 33 targetSdk 33
versionCode 1 versionCode 10
versionName "1.0.0" versionName "1.1.0"
} }
buildTypes { buildTypes {
@@ -98,13 +99,13 @@ dependencies {
implementation "io.insert-koin:koin-android:3.1.3" implementation "io.insert-koin:koin-android:3.1.3"
// Preference // 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'
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx' exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
} }
// Gson // Gson
implementation "com.google.code.gson:gson:2.9.1" implementation "com.google.code.gson:gson:2.10.1"
// Network // Network
implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:retrofit:2.9.0"
@@ -113,8 +114,8 @@ dependencies {
implementation "com.squareup.okhttp3:logging-interceptor:4.9.3" implementation "com.squareup.okhttp3:logging-interceptor:4.9.3"
// RxJava3 // RxJava3
implementation "io.reactivex.rxjava3:rxjava:3.1.3" implementation "io.reactivex.rxjava3:rxjava:3.1.6"
implementation "io.reactivex.rxjava3:rxandroid:3.0.0" implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0" implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// permission // permission
@@ -126,7 +127,7 @@ dependencies {
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.1' implementation 'com.google.android.gms:play-services-oss-licenses:17.0.1'
// Firebase // 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-dynamic-links-ktx'
implementation 'com.google.firebase:firebase-crashlytics-ktx' implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx' implementation 'com.google.firebase:firebase-analytics-ktx'
@@ -148,4 +149,7 @@ dependencies {
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
implementation "com.michalsvec:single-row-calednar:1.0.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 #-renamesourcefileattribute SourceFile
##---------------Begin: proguard configuration for Gson ---------- ### Gson ProGuard and R8 rules which are relevant for all users
# Gson uses generic type information stored in a class file when working with fields. Proguard ### This file is automatically recognized by ProGuard and R8, see https://developer.android.com/build/shrink-code#configuration-files
# removes such information by default, so configure it to keep all of it. ###
### 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 -keepattributes Signature
# For using GSON @Expose annotation # Keep Gson annotations
-keepattributes *Annotation* # 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 ### The following rules are needed for R8 in "full mode" which only adheres to `-keepattribtues` if
-keep class com.google.gson.examples.android.model.** { <fields>; } ### 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, # Keep class TypeToken (respectively its generic signature)
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) -keep class com.google.gson.reflect.TypeToken { *; }
-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
# 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 * { -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 fields with any other Gson annotation
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken # Also allow obfuscation, assuming that users will additionally use @SerializedName or
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken # 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 public class * implements com.bumptech.glide.module.GlideModule
-keep class * extends com.bumptech.glide.module.AppGlideModule { -keep class * extends com.bumptech.glide.module.AppGlideModule {
@@ -112,30 +150,59 @@
@retrofit2.http.* <methods>; @retrofit2.http.* <methods>;
} }
# Ignore annotation used for build tooling.
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
# Ignore JSR 305 annotations for embedding nullability information. # Ignore JSR 305 annotations for embedding nullability information.
-dontwarn javax.annotation.** -dontwarn javax.annotation.**
# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. # Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
-dontwarn kotlin.Unit -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 # 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. # and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
-if interface * { @retrofit2.http.* <methods>; } -if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface <1> -keep,allowobfuscation interface <1>
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). # Keep inherited services.
-keep,allowobfuscation,allowshrinking interface retrofit2.Call -if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation,allowshrinking class retrofit2.Response -keep,allowobfuscation interface * extends <1>
# With R8 full mode generic signatures are stripped for classes that are not # With R8 full mode generic signatures are stripped for classes that are not
# kept. Suspend functions are wrapped in continuations where the type argument # kept. Suspend functions are wrapped in continuations where the type argument
# is used. # is used.
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation -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.Files
-dontwarn java.nio.file.Path -dontwarn java.nio.file.Path
-dontwarn java.nio.file.OpenOption -dontwarn java.nio.file.OpenOption
-dontwarn com.google.devtools.build.android.desugar.runtime.ThrowableExtension
-keep class io.agora.**{*;} -keep class io.agora.**{*;}
-dontwarn org.codehaus.mojo.** -dontwarn org.codehaus.mojo.**
@@ -152,3 +219,12 @@
-keep class androidx.recyclerview.widget.**{*;} -keep class androidx.recyclerview.widget.**{*;}
-keep class androidx.viewpager2.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="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" /> <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 <application
android:name=".app.SodaLiveApp" android:name=".app.SodaLiveApp"
android:allowBackup="true" android:allowBackup="true"
@@ -50,6 +65,17 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </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>
<activity android:name=".main.MainActivity" /> <activity android:name=".main.MainActivity" />
<activity android:name=".user.login.LoginActivity" /> <activity android:name=".user.login.LoginActivity" />
@@ -62,7 +88,9 @@
<activity android:name=".live.room.create.LiveRoomCreateActivity" /> <activity android:name=".live.room.create.LiveRoomCreateActivity" />
<activity android:name=".live.room.update.LiveRoomEditActivity" /> <activity android:name=".live.room.update.LiveRoomEditActivity" />
<activity android:name=".live.reservation.complete.LiveReservationCompleteActivity" /> <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.UserProfileActivity" />
<activity android:name=".explorer.profile.donation.UserProfileDonationAllViewActivity" /> <activity android:name=".explorer.profile.donation.UserProfileDonationAllViewActivity" />
<activity android:name=".explorer.profile.fantalk.UserProfileFantalkAllViewActivity" /> <activity android:name=".explorer.profile.fantalk.UserProfileFantalkAllViewActivity" />
@@ -89,6 +117,13 @@
<activity android:name=".live.now.all.LiveNowAllActivity" /> <activity android:name=".live.now.all.LiveNowAllActivity" />
<activity android:name=".live.reservation.all.LiveReservationAllActivity" /> <activity android:name=".live.reservation.all.LiveReservationAllActivity" />
<activity android:name=".mypage.service_center.ServiceCenterActivity" /> <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 <activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity" android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"

View File

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

View File

@@ -1,14 +1,18 @@
package kr.co.vividnext.sodalive.audio_content package kr.co.vividnext.sodalive.audio_content
import io.reactivex.rxjava3.core.Single 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.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.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.GetAudioContentDetailResponse
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest 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.detail.PutAudioContentLikeResponse
import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationRequest 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.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainResponse 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.GetAudioContentOrderListResponse
import kr.co.vividnext.sodalive.audio_content.order.OrderRequest import kr.co.vividnext.sodalive.audio_content.order.OrderRequest
import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse
@@ -132,9 +136,50 @@ interface AudioContentApi {
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>> ): 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") @POST("/audio-content/donation")
fun donation( fun donation(
@Body request: AudioContentDonationRequest, @Body request: AudioContentDonationRequest,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<Any>> ): 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 -> { MusicAction.PLAY.name -> {
if (!isPlaying) { if (!isPlaying && this::mediaPlayer.isInitialized) {
mediaPlayer.start() mediaPlayer.start()
toggleIsPlaying() toggleIsPlaying()
updateNotification() updateNotification()
@@ -154,7 +154,7 @@ class AudioContentPlayService :
} }
MusicAction.PAUSE.name -> { MusicAction.PAUSE.name -> {
if (isPlaying) { if (isPlaying && this::mediaPlayer.isInitialized) {
mediaPlayer.pause() mediaPlayer.pause()
toggleIsPlaying() toggleIsPlaying()
updateNotification() updateNotification()
@@ -186,11 +186,14 @@ class AudioContentPlayService :
MusicAction.PROGRESS.name -> { MusicAction.PROGRESS.name -> {
val progress = intent.getIntExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, 0) val progress = intent.getIntExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, 0)
if (progress > 0) { if (progress > 0 && this::mediaPlayer.isInitialized) {
if (contentId != null) saveNewPlaybackTracking( if (contentId != null) {
totalDuration = mediaPlayer.duration, saveNewPlaybackTracking(
progress = progress totalDuration = mediaPlayer.duration,
) progress = progress
)
}
mediaPlayer.seekTo(progress) mediaPlayer.seekTo(progress)
} }
} }
@@ -258,7 +261,7 @@ class AudioContentPlayService :
} }
) )
if (isPlaying) { if (isPlaying && this::mediaPlayer.isInitialized) {
mediaPlayer.stop() mediaPlayer.stop()
setEndPositionPlaybackTracking(mediaPlayer.currentPosition) setEndPositionPlaybackTracking(mediaPlayer.currentPosition)
@@ -290,12 +293,12 @@ class AudioContentPlayService :
return null return null
} }
override fun onCompletion(mp: MediaPlayer?) { override fun onCompletion(mp: MediaPlayer) {
setEndPositionPlaybackTracking(mediaPlayer.currentPosition) setEndPositionPlaybackTracking(mp.currentPosition)
if (SharedPreferenceManager.isContentPlayLoop) { if (SharedPreferenceManager.isContentPlayLoop) {
saveNewPlaybackTracking(totalDuration = mediaPlayer.duration, progress = 0) saveNewPlaybackTracking(totalDuration = mp.duration, progress = 0)
mediaPlayer.start() mp.start()
} else { } else {
toggleIsPlaying(false) toggleIsPlaying(false)
mediaPlayer.release() mediaPlayer.release()
@@ -370,61 +373,63 @@ class AudioContentPlayService :
} }
override fun onPrepared(mp: MediaPlayer?) { override fun onPrepared(mp: MediaPlayer?) {
saveNewPlaybackTracking(totalDuration = mediaPlayer.duration, progress = 0) if (this::mediaPlayer.isInitialized) {
sendBroadcast( saveNewPlaybackTracking(totalDuration = mediaPlayer.duration, progress = 0)
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER) sendBroadcast(
.apply { Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
putExtra( .apply {
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION, putExtra(
MusicAction.PLAY Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
) MusicAction.PLAY
)
putExtra( putExtra(
Constants.EXTRA_AUDIO_CONTENT_DURATION, Constants.EXTRA_AUDIO_CONTENT_DURATION,
mediaPlayer.duration mediaPlayer.duration
) )
putExtra( putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID, Constants.EXTRA_AUDIO_CONTENT_ID,
contentId contentId
) )
putExtra( putExtra(
Constants.EXTRA_AUDIO_CONTENT_ALERT_PREVIEW, Constants.EXTRA_AUDIO_CONTENT_ALERT_PREVIEW,
true true
) )
} }
) )
sendBroadcast( sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER) Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply { .apply {
putExtra( putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING, Constants.EXTRA_AUDIO_CONTENT_PLAYING,
false false
) )
putExtra( putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING, Constants.EXTRA_AUDIO_CONTENT_SHOWING,
true true
) )
putExtra( putExtra(
Constants.EXTRA_AUDIO_CONTENT_TITLE, Constants.EXTRA_AUDIO_CONTENT_TITLE,
title title
) )
putExtra( putExtra(
Constants.EXTRA_NICKNAME, Constants.EXTRA_NICKNAME,
nickname nickname
) )
putExtra( putExtra(
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL, Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL,
coverImageUrl coverImageUrl
) )
} }
) )
}
} }
private fun updateNotification() { private fun updateNotification() {
@@ -463,7 +468,7 @@ class AudioContentPlayService :
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) { override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
val notificationBuilder = NotificationCompat val notificationBuilder = NotificationCompat
.Builder(this@AudioContentPlayService, channelId) .Builder(this@AudioContentPlayService, channelId)
.setSmallIcon(R.drawable.ic_noti) .setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(resource) .setLargeIcon(resource)
.setContentTitle(title ?: "오디오 콘텐츠") .setContentTitle(title ?: "오디오 콘텐츠")
.setContentText(nickname ?: "") .setContentText(nickname ?: "")

View File

@@ -14,6 +14,20 @@ class AudioContentRepository(
private val api: AudioContentApi, private val api: AudioContentApi,
private val userApi: UserApi 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( fun getAudioContentList(
id: Long, id: Long,
page: Int, page: Int,
@@ -122,6 +136,20 @@ class AudioContentRepository(
authHeader = token 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( fun donation(
contentId: Long, contentId: Long,
can: Int, can: Int,
@@ -135,4 +163,18 @@ class AudioContentRepository(
), ),
authHeader = token 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 package kr.co.vividnext.sodalive.audio_content.comment
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R 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.ItemAudioContentCommentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentCommentAdapter( class AudioContentCommentAdapter(
private val creatorId: Long,
private val modifyComment: (Long, String) -> Unit,
private val onClickDelete: (Long) -> Unit,
private val onItemClick: (GetAudioContentCommentListItem) -> Unit private val onItemClick: (GetAudioContentCommentListItem) -> Unit
) : RecyclerView.Adapter<AudioContentCommentAdapter.ViewHolder>() { ) : RecyclerView.Adapter<AudioContentCommentAdapter.ViewHolder>() {
var items = mutableSetOf<GetAudioContentCommentListItem>() var items = mutableSetOf<GetAudioContentCommentListItem>()
inner class ViewHolder( inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentCommentBinding private val binding: ItemAudioContentCommentBinding
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
@@ -30,34 +37,34 @@ class AudioContentCommentAdapter(
} }
val tvCommentLayoutParams = binding.tvComment.layoutParams as LinearLayout.LayoutParams val tvCommentLayoutParams = binding.tvComment.layoutParams as LinearLayout.LayoutParams
val coin = item.donationCoin val can = item.donationCan
if (coin > 0) { if (can > 0) {
tvCommentLayoutParams.topMargin = 0 tvCommentLayoutParams.topMargin = 0
binding.llDonationCoin.visibility = View.VISIBLE binding.llDonationCan.visibility = View.VISIBLE
binding.tvDonationCoin.text = coin.moneyFormat() binding.tvDonationCan.text = can.moneyFormat()
binding.llDonationCoin.setBackgroundResource( binding.llDonationCan.setBackgroundResource(
when { when {
coin >= 100000 -> { can >= 100000 -> {
R.drawable.bg_round_corner_10_7_973a3a R.drawable.bg_round_corner_10_7_973a3a
} }
coin >= 50000 -> { can >= 50000 -> {
R.drawable.bg_round_corner_10_7_d85e37 R.drawable.bg_round_corner_10_7_d85e37
} }
coin >= 10000 -> { can >= 10000 -> {
R.drawable.bg_round_corner_10_7_d38c38 R.drawable.bg_round_corner_10_7_d38c38
} }
coin >= 5000 -> { can >= 5000 -> {
R.drawable.bg_round_corner_10_7_59548f R.drawable.bg_round_corner_10_7_59548f
} }
coin >= 1000 -> { can >= 1000 -> {
R.drawable.bg_round_corner_10_7_4d6aa4 R.drawable.bg_round_corner_10_7_4d6aa4
} }
coin >= 500 -> { can >= 500 -> {
R.drawable.bg_round_corner_10_7_2d7390 R.drawable.bg_round_corner_10_7_2d7390
} }
@@ -68,7 +75,7 @@ class AudioContentCommentAdapter(
) )
} else { } else {
tvCommentLayoutParams.topMargin = 13.3f.dpToPx().toInt() tvCommentLayoutParams.topMargin = 13.3f.dpToPx().toInt()
binding.llDonationCoin.visibility = View.GONE binding.llDonationCan.visibility = View.GONE
} }
binding.tvComment.layoutParams = tvCommentLayoutParams 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.tvWriteReply.setOnClickListener { onItemClick(item) }
binding.root.setOnClickListener { onItemClick(item) } binding.root.setOnClickListener { onItemClick(item) }
} }
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemAudioContentCommentBinding.inflate( ItemAudioContentCommentBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
@@ -100,4 +137,38 @@ class AudioContentCommentAdapter(
} }
override fun getItemCount() = items.size 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.R
import kr.co.vividnext.sodalive.databinding.DialogAudioContentCommentBinding 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 private lateinit var binding: DialogAudioContentCommentBinding
@@ -46,6 +49,7 @@ class AudioContentCommentFragment(private val audioContentId: Long) : BottomShee
val commentListFragmentTag = "COMMENT_LIST_FRAGMENT" val commentListFragmentTag = "COMMENT_LIST_FRAGMENT"
val commentListFragment = AudioContentCommentListFragment.newInstance( val commentListFragment = AudioContentCommentListFragment.newInstance(
creatorId = creatorId,
audioContentId = audioContentId audioContentId = audioContentId
) )
val fragmentTransaction = childFragmentManager.beginTransaction() val fragmentTransaction = childFragmentManager.beginTransaction()
@@ -61,6 +65,7 @@ class AudioContentCommentFragment(private val audioContentId: Long) : BottomShee
fun onClickComment(comment: GetAudioContentCommentListItem) { fun onClickComment(comment: GetAudioContentCommentListItem) {
val commentReplyFragmentTag = "COMMENT_REPLY_FRAGMENT" val commentReplyFragmentTag = "COMMENT_REPLY_FRAGMENT"
val commentReplyFragment = AudioContentCommentReplyFragment.newInstance( val commentReplyFragment = AudioContentCommentReplyFragment.newInstance(
creatorId = creatorId,
audioContentId = audioContentId, audioContentId = audioContentId,
comment = comment comment = comment
) )

View File

@@ -15,6 +15,7 @@ import coil.load
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment 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.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
@@ -32,6 +33,7 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
private lateinit var loadingDialog: LoadingDialog private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentCommentAdapter private lateinit var adapter: AudioContentCommentAdapter
private var creatorId: Long = 0
private var audioContentId: Long = 0 private var audioContentId: Long = 0
override fun onCreateView( override fun onCreateView(
@@ -39,6 +41,7 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
creatorId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID) ?: 0
audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0 audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0
return super.onCreateView(inflater, container, savedInstanceState) return super.onCreateView(inflater, container, savedInstanceState)
} }
@@ -63,7 +66,7 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) { binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
@@ -74,9 +77,38 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
viewModel.registerComment(audioContentId, comment) viewModel.registerComment(audioContentId, comment)
} }
adapter = AudioContentCommentAdapter { adapter = AudioContentCommentAdapter(
(parentFragment as AudioContentCommentFragment).onClickComment(it) 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 val recyclerView = binding.rvComment
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
@@ -170,8 +202,9 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
} }
companion object { companion object {
fun newInstance(audioContentId: Long): AudioContentCommentListFragment { fun newInstance(creatorId: Long, audioContentId: Long): AudioContentCommentListFragment {
val args = Bundle() val args = Bundle()
args.putLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, creatorId)
args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId) args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
val fragment = AudioContentCommentListFragment() val fragment = AudioContentCommentListFragment()

View File

@@ -48,11 +48,12 @@ class AudioContentCommentListViewModel(
if (it.success && it.data != null) { if (it.success && it.data != null) {
_totalCommentCount.postValue(it.data.totalCount) _totalCommentCount.postValue(it.data.totalCount)
page += 1
if (it.data.items.isNotEmpty()) { if (it.data.items.isNotEmpty()) {
page += 1
_commentList.postValue(it.data.items) _commentList.postValue(it.data.items)
} else { } else {
isLast = true isLast = true
_commentList.postValue(listOf())
} }
} else { } else {
if (it.message != null) { 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 package kr.co.vividnext.sodalive.audio_content.comment
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import coil.load import coil.load
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R 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.ItemAudioContentCommentBinding
import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentReplyBinding import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentReplyBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentCommentReplyAdapter : class AudioContentCommentReplyAdapter(
RecyclerView.Adapter<AudioContentCommentReplyViewHolder>() { private val creatorId: Long,
private val modifyComment: (Long, String) -> Unit,
private val onClickDelete: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentCommentReplyViewHolder>() {
var items = mutableSetOf<GetAudioContentCommentListItem>() var items = mutableSetOf<GetAudioContentCommentListItem>()
@@ -22,19 +31,25 @@ class AudioContentCommentReplyAdapter :
): AudioContentCommentReplyViewHolder { ): AudioContentCommentReplyViewHolder {
return if (viewType == 0) { return if (viewType == 0) {
AudioContentCommentReplyHeaderViewHolder( AudioContentCommentReplyHeaderViewHolder(
ItemAudioContentCommentBinding.inflate( binding = ItemAudioContentCommentBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
false false
) ),
) )
} else { } else {
AudioContentCommentReplyItemViewHolder( AudioContentCommentReplyItemViewHolder(
context = parent.context,
creatorId = creatorId,
ItemAudioContentCommentReplyBinding.inflate( ItemAudioContentCommentReplyBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
false 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 { override fun getItemViewType(position: Int): Int {
return position 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( abstract class AudioContentCommentReplyViewHolder(
@@ -63,31 +112,109 @@ class AudioContentCommentReplyHeaderViewHolder(
override fun bind(item: GetAudioContentCommentListItem) { override fun bind(item: GetAudioContentCommentListItem) {
binding.ivCommentProfile.load(item.profileUrl) { binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation()) 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.tvComment.text = item.comment
binding.tvCommentDate.text = item.date binding.tvCommentDate.text = item.date
binding.tvCommentNickname.text = item.nickname binding.tvCommentNickname.text = item.nickname
binding.tvWriteReply.visibility = View.GONE binding.tvWriteReply.visibility = View.GONE
binding.ivMenu.visibility = View.GONE
} }
} }
class AudioContentCommentReplyItemViewHolder( 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) { ) : AudioContentCommentReplyViewHolder(binding) {
override fun bind(item: GetAudioContentCommentListItem) { override fun bind(item: GetAudioContentCommentListItem) {
binding.ivCommentProfile.load(item.profileUrl) { binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
binding.tvComment.text = item.comment binding.tvComment.text = item.comment
binding.tvCommentDate.text = item.date binding.tvCommentDate.text = item.date
binding.tvCommentNickname.text = item.nickname 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 coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment 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.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
@@ -34,6 +35,8 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
private lateinit var adapter: AudioContentCommentReplyAdapter private lateinit var adapter: AudioContentCommentReplyAdapter
private var originalComment: GetAudioContentCommentListItem? = null private var originalComment: GetAudioContentCommentListItem? = null
private var creatorId: Long = 0
private var audioContentId: Long = 0 private var audioContentId: Long = 0
override fun onCreateView( override fun onCreateView(
@@ -41,6 +44,7 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
creatorId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID) ?: 0
audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0 audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0
originalComment = BundleCompat.getParcelable( originalComment = BundleCompat.getParcelable(
requireArguments(), requireArguments(),
@@ -83,7 +87,7 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) { binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
@@ -94,7 +98,35 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
viewModel.registerComment(audioContentId, originalComment!!.id, comment) 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!!) items.add(originalComment!!)
} }
@@ -189,11 +221,13 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
companion object { companion object {
fun newInstance( fun newInstance(
creatorId: Long,
audioContentId: Long, audioContentId: Long,
comment: GetAudioContentCommentListItem comment: GetAudioContentCommentListItem
): AudioContentCommentReplyFragment { ): AudioContentCommentReplyFragment {
val args = Bundle() val args = Bundle()
args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId) 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) args.putParcelable(Constants.EXTRA_AUDIO_CONTENT_COMMENT, comment)
val fragment = AudioContentCommentReplyFragment() val fragment = AudioContentCommentReplyFragment()

View File

@@ -42,11 +42,12 @@ class AudioContentCommentReplyViewModel(
.subscribe( .subscribe(
{ {
if (it.success && it.data != null) { if (it.success && it.data != null) {
page += 1
if (it.data.items.isNotEmpty()) { if (it.data.items.isNotEmpty()) {
page += 1
_commentList.postValue(it.data.items) _commentList.postValue(it.data.items)
} else { } else {
isLast = true isLast = true
_commentList.postValue(listOf())
} }
} else { } else {
if (it.message != null) { 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, timezone = TimeZone.getDefault().id,
authHeader = token authHeader = token
) )
fun modifyComment(request: ModifyCommentRequest, token: String) = api.modifyComment(
request = request,
authHeader = token
)
} }

View File

@@ -12,10 +12,11 @@ data class GetAudioContentCommentListResponse(
@Parcelize @Parcelize
data class GetAudioContentCommentListItem( data class GetAudioContentCommentListItem(
@SerializedName("id") val id: Long, @SerializedName("id") val id: Long,
@SerializedName("writerId") val writerId: Long,
@SerializedName("nickname") val nickname: String, @SerializedName("nickname") val nickname: String,
@SerializedName("profileUrl") val profileUrl: String, @SerializedName("profileUrl") val profileUrl: String,
@SerializedName("comment") val comment: String, @SerializedName("comment") val comment: String,
@SerializedName("donationCoin") val donationCoin: Int, @SerializedName("donationCan") val donationCan: Int,
@SerializedName("date") val date: String, @SerializedName("date") val date: String,
@SerializedName("replyCount") val replyCount: Int @SerializedName("replyCount") val replyCount: Int
) : Parcelable ) : 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.Auth
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse 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 kr.co.vividnext.sodalive.report.ReportType
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
@@ -56,6 +57,8 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
private var isAlertPreview = false private var isAlertPreview = false
private val audioContentReceiver = AudioContentReceiver() private val audioContentReceiver = AudioContentReceiver()
private var creatorId: Long = 0
private var refresh = false private var refresh = false
set(value) { set(value) {
field = value field = value
@@ -69,7 +72,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.scrollView.scrollTo(0, 0) binding.scrollView.scrollTo(0, 0)
binding.sbProgress.progress = 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.tvTotalDuration.text = " / 00:00:00"
binding.tvCurrentDuration.text = "00:00:00" binding.tvCurrentDuration.text = "00:00:00"
binding.rlPreviewAlert.visibility = View.GONE binding.rlPreviewAlert.visibility = View.GONE
@@ -102,8 +105,8 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
} }
override fun onPause() { override fun onPause() {
super.onPause()
unregisterReceiver(audioContentReceiver) unregisterReceiver(audioContentReceiver)
super.onPause()
} }
override fun setupView() { override fun setupView() {
@@ -248,7 +251,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
LayoutInflater.from(this) LayoutInflater.from(this)
) { can, message -> ) { can, message ->
if (can <= 0) { if (can <= 0) {
showToast("1코인 이상 후원하실 수 있습니다.") showToast("1 이상 후원하실 수 있습니다.")
} else if (message.isBlank()) { } else if (message.isBlank()) {
showToast("함께 보낼 메시지를 입력하세요.") showToast("함께 보낼 메시지를 입력하세요.")
} else { } else {
@@ -260,8 +263,8 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
} }
} }
private fun donation(coin: Int, message: String) { private fun donation(can: Int, message: String) {
viewModel.donation(audioContentId, coin, message) { viewModel.donation(audioContentId, can, message) {
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() } viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
} }
} }
@@ -388,6 +391,14 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
isAlertPreview = it.creator.creatorId != SharedPreferenceManager.userId && isAlertPreview = it.creator.creatorId != SharedPreferenceManager.userId &&
!it.existOrdered && !it.existOrdered &&
it.price > 0 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) { viewModel.isContentPlayLoopLiveData.observe(this) {
@@ -474,7 +485,10 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
} }
private fun showCommentBottomSheetDialog() { private fun showCommentBottomSheetDialog() {
val dialog = AudioContentCommentFragment(audioContentId = audioContentId) val dialog = AudioContentCommentFragment(
creatorId = creatorId,
audioContentId = audioContentId
)
dialog.show( dialog.show(
supportFragmentManager, supportFragmentManager,
dialog.tag dialog.tag
@@ -491,8 +505,14 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.llPurchase.visibility = View.VISIBLE binding.llPurchase.visibility = View.VISIBLE
binding.tvPrice.text = response.price.toString() binding.tvPrice.text = response.price.toString()
binding.tvStrPurchaseOrRental.text = if (response.isOnlyRental) {
" 대여하기"
} else {
" 구매하기"
}
binding.llPurchase.setOnClickListener { binding.llPurchase.setOnClickListener {
showOrderDialog(audioContent = response) showOrderDialog(audioContent = response, isOnlyRental = response.isOnlyRental)
} }
} else { } else {
binding.llPurchase.visibility = View.GONE binding.llPurchase.visibility = View.GONE
@@ -629,6 +649,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
} }
private fun setupCreatorArea(creator: AudioContentCreator) { private fun setupCreatorArea(creator: AudioContentCreator) {
this.creatorId = creator.creatorId
binding.rlProfile.setOnClickListener { binding.rlProfile.setOnClickListener {
startActivity( startActivity(
Intent(applicationContext, UserProfileActivity::class.java).apply { 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( val dialog = AudioContentOrderFragment(
price = audioContent.price, price = audioContent.price,
onClickKeep = { showOrderConfirmDialog(audioContent, OrderType.KEEP) }, isOnlyRental = isOnlyRental,
onClickRental = { showOrderConfirmDialog(audioContent, OrderType.RENTAL) } onClickKeep = { showOrderConfirmDialog(audioContent, isOnlyRental, OrderType.KEEP) },
onClickRental = { showOrderConfirmDialog(audioContent, isOnlyRental, OrderType.RENTAL) }
) )
dialog.show( dialog.show(
@@ -685,6 +710,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
private fun showOrderConfirmDialog( private fun showOrderConfirmDialog(
audioContent: GetAudioContentDetailResponse, audioContent: GetAudioContentDetailResponse,
isOnlyRental: Boolean = false,
orderType: OrderType orderType: OrderType
) { ) {
AudioContentOrderConfirmDialog( AudioContentOrderConfirmDialog(
@@ -693,10 +719,10 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
title = audioContent.title, title = audioContent.title,
theme = audioContent.themeStr, theme = audioContent.themeStr,
coverImageUrl = audioContent.coverImageUrl, coverImageUrl = audioContent.coverImageUrl,
isAdult = audioContent.isAdult,
profileImageUrl = audioContent.creator.profileImageUrl, profileImageUrl = audioContent.creator.profileImageUrl,
nickname = audioContent.creator.nickname, nickname = audioContent.creator.nickname,
duration = audioContent.duration, duration = audioContent.duration,
isOnlyRental = isOnlyRental,
orderType = orderType, orderType = orderType,
price = audioContent.price, price = audioContent.price,
confirmButtonClick = { confirmButtonClick = {
@@ -712,7 +738,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
viewModel.order( viewModel.order(
contentId = audioContent.contentId, contentId = audioContent.contentId,
orderType = orderType orderType = orderType
) ) {
val intent = Intent(applicationContext, CanChargeActivity::class.java)
intent.putExtra(Constants.EXTRA_GO_TO_PREV_PAGE, true)
startActivity(intent)
}
}, },
).show(screenWidth) ).show(screenWidth)
} }
@@ -752,7 +782,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
if (isPlaying != null && isPlaying) { if (isPlaying != null && isPlaying) {
R.drawable.btn_audio_content_pause R.drawable.btn_audio_content_pause
} else { } 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!! _isShowPreviewAlert.value = !_isShowPreviewAlert.value!!
} }
fun order(contentId: Long, orderType: OrderType) { fun order(contentId: Long, orderType: OrderType, gotoShop: () -> Unit) {
isLoading.value = true isLoading.value = true
compositeDisposable.add( compositeDisposable.add(
repository.orderContent( repository.orderContent(
@@ -190,10 +190,19 @@ class AudioContentDetailViewModel(
{ {
if (it.success && it.data != null) { if (it.success && it.data != null) {
getAudioContentDetail(audioContentId = contentId) getAudioContentDetail(audioContentId = contentId)
_toastLiveData.postValue("구매가 완료되었습니다.") _toastLiveData.postValue(
if (orderType == OrderType.RENTAL) {
"대여가 완료되었습니다."
} else {
"구매가 완료되었습니다."
}
)
} else { } else {
if (it.message != null) { if (it.message != null) {
_toastLiveData.postValue(it.message) _toastLiveData.postValue(it.message)
if (it.message.contains("캔이 부족합니다")) {
gotoShop()
}
} else { } else {
_toastLiveData.postValue( _toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
@@ -332,15 +341,15 @@ class AudioContentDetailViewModel(
) { ) {
isLoading.value = true isLoading.value = true
Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) { Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) {
link = Uri.parse("https://yozm.day/?audio_content_id=$audioContentId") link = Uri.parse("https://sodalive.net/?audio_content_id=$audioContentId")
domainUriPrefix = "https://yozm.page.link" domainUriPrefix = "https://sodalive.page.link"
androidParameters { } androidParameters { }
iosParameters("kr.co.vividnext.yozm") { iosParameters("kr.co.vividnext.sodalive") {
appStoreId = "1630284226" appStoreId = "6461721697"
} }
socialMetaTagParameters { socialMetaTagParameters {
title = contentTitle title = contentTitle
description = "지금 요즘라이브에서 이 콘텐츠 감상하기" description = "지금 소다라이브에서 이 콘텐츠 감상하기"
imageUrl = contentImage.toUri() imageUrl = contentImage.toUri()
} }
}.addOnSuccessListener { }.addOnSuccessListener {

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.audio_content.main package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
@@ -10,12 +11,17 @@ import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.zhpan.bannerview.BaseBannerAdapter import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.indicator.enums.IndicatorSlideMode import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle 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.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.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity 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 orderListAdapter: AudioContentMainContentAdapter
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentAdapter: AudioContentMainContentAdapter private lateinit var newContentAdapter: AudioContentMainContentAdapter
private lateinit var contentRankingSortAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var contentRankingAdapter: AudioContentMainRankingAdapter
private lateinit var curationAdapter: AudioContentMainCurationAdapter private lateinit var curationAdapter: AudioContentMainCurationAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -80,12 +88,18 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
setupOrderList() setupOrderList()
setupNewContentTheme() setupNewContentTheme()
setupNewContent() setupNewContent()
setupContentRankingSortType()
setupContentRanking()
setupCuration() setupCuration()
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
viewModel.getMain() viewModel.getMain()
} }
binding.ivCanFree.setOnClickListener {
PointClickAd.showOfferwall(requireActivity(), "무료충전")
}
} }
private fun setupNewContentCreator() { private fun setupNewContentCreator() {
@@ -116,7 +130,7 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
outRect.right = 10.7f.dpToPx().toInt() outRect.right = 10.7f.dpToPx().toInt()
} }
orderListAdapter.itemCount - 1 -> { newContentCreatorAdapter.itemCount - 1 -> {
outRect.left = 10.7f.dpToPx().toInt() outRect.left = 10.7f.dpToPx().toInt()
outRect.right = 0 outRect.right = 0
} }
@@ -142,7 +156,11 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
layoutParams.width = pagerWidth.roundToInt() layoutParams.width = pagerWidth.roundToInt()
layoutParams.height = pagerHeight layoutParams.height = pagerHeight
bannerAdapter = AudioContentMainBannerAdapter(pagerWidth.roundToInt(), pagerHeight) { bannerAdapter = AudioContentMainBannerAdapter(
requireContext(),
pagerWidth.roundToInt(),
pagerHeight
) {
when (it.type) { when (it.type) {
AudioContentBannerType.EVENT -> { AudioContentBannerType.EVENT -> {
startActivity( startActivity(
@@ -276,7 +294,7 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
outRect.right = 4f.dpToPx().toInt() outRect.right = 4f.dpToPx().toInt()
} }
orderListAdapter.itemCount - 1 -> { newContentThemeAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt() outRect.left = 4f.dpToPx().toInt()
outRect.right = 0 outRect.right = 0
} }
@@ -293,6 +311,10 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
} }
private fun setupNewContent() { private fun setupNewContent() {
binding.ivNewContentAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentNewAllActivity::class.java))
}
newContentAdapter = AudioContentMainContentAdapter( newContentAdapter = AudioContentMainContentAdapter(
onClickItem = { onClickItem = {
startActivity( startActivity(
@@ -331,7 +353,7 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
outRect.right = 6.7f.dpToPx().toInt() outRect.right = 6.7f.dpToPx().toInt()
} }
orderListAdapter.itemCount - 1 -> { newContentAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt() outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0 outRect.right = 0
} }
@@ -347,6 +369,88 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
binding.rvNewContent.adapter = newContentAdapter 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() { private fun setupCuration() {
curationAdapter = AudioContentMainCurationAdapter( curationAdapter = AudioContentMainCurationAdapter(
onClickItem = { onClickItem = {
@@ -362,6 +466,15 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
putExtra(Constants.EXTRA_USER_ID, it) 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 binding.rvCuration.adapter = curationAdapter
} }
@SuppressLint("SetTextI18n")
private fun bindData() { private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) { viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) { if (it) {
@@ -466,5 +580,16 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
View.VISIBLE 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 package kr.co.vividnext.sodalive.audio_content.main
import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
@@ -17,13 +16,13 @@ class AudioContentMainItemViewHolder(
fun bind(item: GetAudioContentMainItem) { fun bind(item: GetAudioContentMainItem) {
binding.ivAudioContentCoverImage.load(item.coverImageUrl) { binding.ivAudioContentCoverImage.load(item.coverImageUrl) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(2.7f.dpToPx())) transformations(RoundedCornersTransformation(2.7f.dpToPx()))
} }
binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) { binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
@@ -31,11 +30,6 @@ class AudioContentMainItemViewHolder(
binding.tvAudioContentCreatorNickname.text = item.creatorNickname binding.tvAudioContentCreatorNickname.text = item.creatorNickname
binding.ivAudioContentCreator.setOnClickListener { onClickCreator(item.creatorId) } binding.ivAudioContentCreator.setOnClickListener { onClickCreator(item.creatorId) }
binding.iv19.visibility = if (item.isAdult) {
View.VISIBLE
} else {
View.GONE
}
binding.root.setOnClickListener { onClickItem(item.contentId) } binding.root.setOnClickListener { onClickItem(item.contentId) }
} }
} }

View File

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

View File

@@ -22,7 +22,11 @@ class AudioContentMainNewContentThemeAdapter(
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun bind(theme: String) { fun bind(theme: String) {
if (theme == selectedTheme || (selectedTheme == "" && theme == "전체")) { if (
theme == selectedTheme ||
(selectedTheme == "" && theme == "전체") ||
(selectedTheme == "" && theme == "매출")
) {
binding.tvTheme.setBackgroundResource( binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_9970ff 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>> val curationListLiveData: LiveData<List<GetAudioContentCurationResponse>>
get() = _curationListLiveData 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() { fun getMain() {
_isLoading.value = true _isLoading.value = true
compositeDisposable.add( compositeDisposable.add(
@@ -61,6 +69,8 @@ class AudioContentMainViewModel(
_orderListLiveData.value = data.orderList _orderListLiveData.value = data.orderList
_bannerLiveData.value = data.bannerList _bannerLiveData.value = data.bannerList
_curationListLiveData.value = data.curationList _curationListLiveData.value = data.curationList
_contentRankingLiveData.value = data.contentRanking
_contentRankingSortListLiveData.value = data.contentRankingSortTypeList
val themeList = listOf("전체").union(data.themeList).toList() val themeList = listOf("전체").union(data.themeList).toList()
_themeListLiveData.value = themeList _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("orderList") val orderList: List<GetAudioContentMainItem>,
@SerializedName("themeList") val themeList: List<String>, @SerializedName("themeList") val themeList: List<String>,
@SerializedName("newContentList") val newContentList: List<GetAudioContentMainItem>, @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( data class GetNewContentUploadCreator(
@@ -23,16 +25,33 @@ data class GetAudioContentMainItem(
@SerializedName("contentId") val contentId: Long, @SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String, @SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("title") val title: String, @SerializedName("title") val title: String,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("creatorId") val creatorId: Long, @SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String, @SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: 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( data class GetAudioContentCurationResponse(
@SerializedName("curationId") val curationId: Long,
@SerializedName("title") val title: String, @SerializedName("title") val title: String,
@SerializedName("description") val description: String, @SerializedName("description") val description: String,
@SerializedName("audioContents") val audioContents: List<GetAudioContentMainItem> @SerializedName("contents") val audioContents: List<GetAudioContentMainItem>
) )
data class GetAudioContentBannerResponse( data class GetAudioContentBannerResponse(

View File

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

View File

@@ -10,6 +10,7 @@ import kotlin.math.ceil
class AudioContentOrderFragment( class AudioContentOrderFragment(
private val price: Int, private val price: Int,
private val isOnlyRental: Boolean,
private val onClickRental: () -> Unit, private val onClickRental: () -> Unit,
private val onClickKeep: () -> Unit private val onClickKeep: () -> Unit
) : BottomSheetDialogFragment() { ) : BottomSheetDialogFragment() {
@@ -28,12 +29,18 @@ class AudioContentOrderFragment(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.tvKeep.text = "$price" if (isOnlyRental) {
binding.tvRental.text = "${ceil(price * 0.7).toInt()}" 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 { binding.rlKeep.visibility = View.VISIBLE
onClickKeep() binding.llKeep.setOnClickListener {
dismiss() onClickKeep()
dismiss()
}
} }
binding.llRental.setOnClickListener { 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 binding.rvOrderList.adapter = adapter
} }
@@ -106,5 +123,9 @@ class AudioContentOrderListActivity : BaseActivity<ActivityAudioContentOrderList
adapter.items.addAll(it) adapter.items.addAll(it)
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
viewModel.totalCount.observe(this) {
binding.tvTotalCount.text = "$it"
}
} }
} }

View File

@@ -23,13 +23,14 @@ class AudioContentOrderListAdapter(
fun bind(item: GetAudioContentOrderListItem) { fun bind(item: GetAudioContentOrderListItem) {
binding.ivCover.load(item.coverImageUrl) { binding.ivCover.load(item.coverImageUrl) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(5.3f.dpToPx())) transformations(RoundedCornersTransformation(5.3f.dpToPx()))
} }
binding.tvTitle.text = item.title binding.tvTitle.text = item.title
binding.tvTheme.text = item.themeStr binding.tvTheme.text = item.themeStr
binding.tvDuration.text = item.duration binding.tvDuration.text = item.duration
binding.tvCreatorNickname.text = item.creatorNickname
binding.tvLikeCount.text = item.likeCount.moneyFormat() binding.tvLikeCount.text = item.likeCount.moneyFormat()
binding.tvCommentCount.text = item.commentCount.moneyFormat() binding.tvCommentCount.text = item.commentCount.moneyFormat()

View File

@@ -25,53 +25,60 @@ class AudioContentOrderListViewModel(
val orderList: LiveData<List<GetAudioContentOrderListItem>> val orderList: LiveData<List<GetAudioContentOrderListItem>>
get() = _orderList get() = _orderList
private var _totalCount = MutableLiveData<Int>()
val totalCount: LiveData<Int>
get() = _totalCount
private var isLast = false private var isLast = false
var page = 1 var page = 1
private val size = 10 private val size = 10
fun getAudioContentOrderList(onFailure: (() -> Unit)? = null) { fun getAudioContentOrderList(onFailure: (() -> Unit)? = null) {
_isLoading.value = true if (_isLoading.value == false) {
compositeDisposable.add( _isLoading.value = true
repository.getAudioContentOrderList( compositeDisposable.add(
page = page, repository.getAudioContentOrderList(
size = size, page = page,
token = "Bearer ${SharedPreferenceManager.token}" size = size,
) token = "Bearer ${SharedPreferenceManager.token}"
.subscribeOn(Schedulers.io()) )
.observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io())
.subscribe( .observeOn(AndroidSchedulers.mainThread())
{ .subscribe(
if (it.success && it.data != null) { {
if (it.data.items.isNotEmpty()) { if (it.success && it.data != null) {
page += 1 _totalCount.value = it.data.totalCount
_orderList.postValue(it.data.items) if (it.data.items.isNotEmpty()) {
page += 1
_orderList.postValue(it.data.items)
} else {
isLast = true
}
} else { } else {
isLast = true if (it.message != null) {
} _toastLiveData.postValue(it.message)
} else { } else {
if (it.message != null) { _toastLiveData.postValue(
_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) { if (onFailure != null) {
onFailure() 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( data class GetAudioContentOrderListItem(
@SerializedName("contentId") val contentId: Long, @SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String, @SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("title") val title: String, @SerializedName("title") val title: String,
@SerializedName("themeStr") val themeStr: String, @SerializedName("themeStr") val themeStr: String,
@SerializedName("duration") val duration: String?, @SerializedName("duration") val duration: String?,

View File

@@ -70,7 +70,7 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
binding.ivCover.background = null binding.ivCover.background = null
binding.ivCover.load(fileUri) { binding.ivCover.load(fileUri) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(13.3f.dpToPx())) transformations(RoundedCornersTransformation(13.3f.dpToPx()))
} }
viewModel.coverImageUri = fileUri viewModel.coverImageUri = fileUri
@@ -169,6 +169,8 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
binding.llPricePaid.setOnClickListener { viewModel.setPriceFree(false) } binding.llPricePaid.setOnClickListener { viewModel.setPriceFree(false) }
binding.llPriceFree.setOnClickListener { viewModel.setPriceFree(true) } 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.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) }
binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) } 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) { viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
} }
@@ -272,51 +300,17 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
viewModel.isPriceFreeLiveData.observe(this) { viewModel.isPriceFreeLiveData.observe(this) {
if (it) { if (it) {
viewModel.price = 0 checkPriceFree()
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
)
} else { } else {
binding.llSetPrice.visibility = View.VISIBLE checkPricePaid()
}
}
binding.ivPricePaid.visibility = View.VISIBLE viewModel.isOnlyRentalLiveData.observe(this) {
binding.tvPricePaid.setTextColor( if (it) {
ContextCompat.getColor( checkOnlyRental()
applicationContext, } else {
R.color.color_eeeeee checkRentalAndKeep()
)
)
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
)
} }
} }
@@ -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? { private fun getFileName(uri: Uri): String? {
val scheme = uri.scheme val scheme = uri.scheme
var fileName: String? = null var fileName: String? = null

View File

@@ -18,6 +18,8 @@ import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink import okio.BufferedSink
import java.io.File import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
class AudioContentUploadViewModel( class AudioContentUploadViewModel(
private val repository: AudioContentRepository private val repository: AudioContentRepository
@@ -35,6 +37,10 @@ class AudioContentUploadViewModel(
val isAdultLiveData: LiveData<Boolean> val isAdultLiveData: LiveData<Boolean>
get() = _isAdultLiveData get() = _isAdultLiveData
private val _isOnlyRentalLiveData = MutableLiveData(false)
val isOnlyRentalLiveData: LiveData<Boolean>
get() = _isOnlyRentalLiveData
private val _isAvailableCommentLiveData = MutableLiveData(true) private val _isAvailableCommentLiveData = MutableLiveData(true)
val isAvailableCommentLiveData: LiveData<Boolean> val isAvailableCommentLiveData: LiveData<Boolean>
get() = _isAvailableCommentLiveData get() = _isAvailableCommentLiveData
@@ -52,6 +58,8 @@ class AudioContentUploadViewModel(
var theme: GetAudioContentThemeResponse? = null var theme: GetAudioContentThemeResponse? = null
var coverImageUri: Uri? = null var coverImageUri: Uri? = null
var contentUri: Uri? = null var contentUri: Uri? = null
var previewStartTime: String? = null
var previewEndTime: String? = null
fun setAdult(isAdult: Boolean) { fun setAdult(isAdult: Boolean) {
_isAdultLiveData.postValue(isAdult) _isAdultLiveData.postValue(isAdult)
@@ -63,6 +71,14 @@ class AudioContentUploadViewModel(
fun setPriceFree(isPriceFree: Boolean) { fun setPriceFree(isPriceFree: Boolean) {
_isPriceFreeLiveData.postValue(isPriceFree) _isPriceFreeLiveData.postValue(isPriceFree)
if (isPriceFree) {
_isOnlyRentalLiveData.postValue(false)
}
}
fun setIsOnlyRental(isOnlyRental: Boolean) {
_isOnlyRentalLiveData.postValue(isOnlyRental)
} }
fun uploadAudioContent(onSuccess: () -> Unit) { fun uploadAudioContent(onSuccess: () -> Unit) {
@@ -76,7 +92,10 @@ class AudioContentUploadViewModel(
price = price, price = price,
themeId = theme!!.id, themeId = theme!!.id,
isAdult = _isAdultLiveData.value!!, isAdult = _isAdultLiveData.value!!,
isCommentAvailable = _isAvailableCommentLiveData.value!! isOnlyRental = _isOnlyRentalLiveData.value!!,
isCommentAvailable = _isAvailableCommentLiveData.value!!,
previewStartTime = previewStartTime,
previewEndTime = previewEndTime
) )
val requestJson = Gson().toJson(request) val requestJson = Gson().toJson(request)
@@ -206,16 +225,86 @@ class AudioContentUploadViewModel(
return false 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) { if (contentUri == null) {
_toastLiveData.postValue("오디오 콘텐츠를 선택해 주세요.") _toastLiveData.postValue("오디오 콘텐츠를 선택해 주세요.")
return false return false
} }
if (!isPriceFreeLiveData.value!! && price < 10) { if (!isPriceFreeLiveData.value!! && price < 5) {
_toastLiveData.postValue("콘텐츠의 최소금액은 10코인 입니다.") _toastLiveData.postValue("콘텐츠의 최소금액은 5캔 입니다.")
return false return false
} }
return true 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("price") val price: Int,
@SerializedName("themeId") val themeId: Long, @SerializedName("themeId") val themeId: Long,
@SerializedName("isAdult") val isAdult: Boolean, @SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isOnlyRental") val isOnlyRental: Boolean,
@SerializedName("isCommentAvailable") val isCommentAvailable: 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) { binding.ivTheme.load(item.image) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }

View File

@@ -8,11 +8,13 @@ object Constants {
const val PREF_IS_ADULT = "pref_is_adult" const val PREF_IS_ADULT = "pref_is_adult"
const val PREF_NICKNAME = "pref_nickname" const val PREF_NICKNAME = "pref_nickname"
const val PREF_USER_ROLE = "pref_user_role" 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_PUSH_TOKEN = "pref_push_token"
const val PREF_PROFILE_IMAGE = "pref_profile_image" const val PREF_PROFILE_IMAGE = "pref_profile_image"
const val PREF_IS_CONTENT_PLAY_LOOP = "pref_is_content_play_loop" 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_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_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_CAN = "extra_can"
const val EXTRA_DATA = "extra_data" const val EXTRA_DATA = "extra_data"
@@ -27,7 +29,7 @@ object Constants {
const val EXTRA_MESSAGE_BOX = "extra_message_box" const val EXTRA_MESSAGE_BOX = "extra_message_box"
const val EXTRA_TEXT_MESSAGE = "extra_text_message" const val EXTRA_TEXT_MESSAGE = "extra_text_message"
const val EXTRA_LIVE_TIME_NOW = "extra_live_time_now" 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_SELECT_RECIPIENT = "extra_select_recipient"
const val EXTRA_ROOM_CHANNEL_NAME = "extra_room_channel_name" const val EXTRA_ROOM_CHANNEL_NAME = "extra_room_channel_name"
const val EXTRA_LIVE_RESERVATION_RESPONSE = "extra_live_reservation_response" 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_COMMENT = "audio_content_comment"
const val EXTRA_AUDIO_CONTENT_LOADING = "audio_content_loading" 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_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_NEXT_ACTION = "audio_content_next_action"
const val EXTRA_AUDIO_CONTENT_ALERT_PREVIEW = "audio_content_alert_preview" const val EXTRA_AUDIO_CONTENT_ALERT_PREVIEW = "audio_content_alert_preview"
const val EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL = "audio_content_cover_image_url" 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.app.Activity
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.AnimationDrawable
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.WindowManager import android.view.WindowManager
@@ -16,14 +15,11 @@ class LoadingDialog(
) { ) {
private val alertDialog: AlertDialog private val alertDialog: AlertDialog
private val dialogView = DialogLoadingBinding.inflate(layoutInflater) private val dialogView = DialogLoadingBinding.inflate(layoutInflater)
private val animationDrawable: AnimationDrawable
init { init {
val dialogBuilder = AlertDialog.Builder(activity) val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root) dialogBuilder.setView(dialogView.root)
animationDrawable = dialogView.tvLoading.compoundDrawables[1] as AnimationDrawable
alertDialog = dialogBuilder.create() alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false) alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
@@ -31,7 +27,6 @@ class LoadingDialog(
fun show(width: Int, message: String = "") { fun show(width: Int, message: String = "") {
alertDialog.show() alertDialog.show()
animationDrawable.start()
dialogView.tvLoading.text = message dialogView.tvLoading.text = message
val lp = WindowManager.LayoutParams() val lp = WindowManager.LayoutParams()
@@ -43,7 +38,6 @@ class LoadingDialog(
} }
fun dismiss() { fun dismiss() {
animationDrawable.stop()
alertDialog.dismiss() alertDialog.dismiss()
} }
} }

View File

@@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.common
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kr.co.vividnext.sodalive.settings.notification.MemberRole import kr.co.vividnext.sodalive.settings.notification.MemberRole
object SharedPreferenceManager { object SharedPreferenceManager {
@@ -116,4 +118,24 @@ object SharedPreferenceManager {
set(value) { set(value) {
sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID] = 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) val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId)
.setSmallIcon(R.drawable.ic_noti) .setSmallIcon(R.drawable.ic_notification)
.setContentTitle(getString(R.string.app_name)) .setContentTitle(getString(R.string.app_name))
.setContentText(content) .setContentText(content)
.setOngoing(true) .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.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
import kr.co.vividnext.sodalive.audio_content.PlaybackTrackingRepository 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.AudioContentCommentListViewModel
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentReplyViewModel 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.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.detail.AudioContentDetailViewModel
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainViewModel import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainViewModel
import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyViewModel 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.ExplorerRepository
import kr.co.vividnext.sodalive.explorer.ExplorerViewModel import kr.co.vividnext.sodalive.explorer.ExplorerViewModel
import kr.co.vividnext.sodalive.explorer.profile.UserProfileViewModel 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.explorer.profile.follow.UserFollowerListViewModel
import kr.co.vividnext.sodalive.following.FollowingCreatorRepository import kr.co.vividnext.sodalive.following.FollowingCreatorRepository
import kr.co.vividnext.sodalive.following.FollowingCreatorViewModel 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.charge.CanChargeViewModel
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentViewModel import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentViewModel
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusViewModel 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.FaqApi
import kr.co.vividnext.sodalive.mypage.service_center.FaqRepository import kr.co.vividnext.sodalive.mypage.service_center.FaqRepository
import kr.co.vividnext.sodalive.mypage.service_center.ServiceCenterViewModel 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(), NoticeApi::class.java) }
single { ApiBuilder().build(get(), AudioContentApi::class.java) } single { ApiBuilder().build(get(), AudioContentApi::class.java) }
single { ApiBuilder().build(get(), FaqApi::class.java) } single { ApiBuilder().build(get(), FaqApi::class.java) }
single { ApiBuilder().build(get(), MemberTagApi::class.java) }
} }
private val viewModelModule = module { private val viewModelModule = module {
@@ -179,6 +190,13 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { AudioContentCommentReplyViewModel(get()) } viewModel { AudioContentCommentReplyViewModel(get()) }
viewModel { FollowingCreatorViewModel(get()) } viewModel { FollowingCreatorViewModel(get()) }
viewModel { ServiceCenterViewModel(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 { private val repositoryModule = module {
@@ -199,6 +217,8 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
factory { PlaybackTrackingRepository(get()) } factory { PlaybackTrackingRepository(get()) }
factory { FollowingCreatorRepository(get(), get()) } factory { FollowingCreatorRepository(get(), get()) }
factory { FaqRepository(get()) } factory { FaqRepository(get()) }
factory { MemberTagRepository(get()) }
factory { UserProfileFantalkAllViewModel(get(), get()) }
} }
private val moduleList = listOf( private val moduleList = listOf(

View File

@@ -27,6 +27,7 @@ class ExplorerAdapter(
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetExplorerSectionResponse) { fun bind(item: GetExplorerSectionResponse) {
setTitle(item) setTitle(item)
setDesc(item)
setCreatorList(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) { private fun setCreatorList(item: GetExplorerSectionResponse) {
val adapter = ExplorerSectionAdapter(onClickItem = onClickItem) val adapter = ExplorerSectionAdapter(
onClickItem = onClickItem,
isVisibleRanking = item.desc != null
)
binding.rvExplorerSection.layoutManager = LinearLayoutManager( binding.rvExplorerSection.layoutManager = LinearLayoutManager(
context, context,

View File

@@ -1,11 +1,14 @@
package kr.co.vividnext.sodalive.explorer package kr.co.vividnext.sodalive.explorer
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse 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.GetCreatorProfileResponse
import kr.co.vividnext.sodalive.explorer.profile.PostCreatorNoticeRequest 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.PostWriteCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest 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.explorer.profile.follow.GetFollowerListResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import retrofit2.http.Body import retrofit2.http.Body
@@ -35,6 +38,23 @@ interface ExplorerApi {
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<GetCreatorProfileResponse>> ): 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") @POST("/explorer/profile/cheers")
fun writeCheers( fun writeCheers(
@Body request: PostWriteCheersRequest, @Body request: PostWriteCheersRequest,

View File

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

View File

@@ -1,8 +1,13 @@
package kr.co.vividnext.sodalive.explorer 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.PostCreatorNoticeRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest 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.cheers.PutModifyCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.donation.GetDonationAllResponse
import java.util.TimeZone import java.util.TimeZone
class ExplorerRepository( class ExplorerRepository(
@@ -21,6 +26,21 @@ class ExplorerRepository(
authHeader = token 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( fun writeCheers(
parentCheersId: Long?, parentCheersId: Long?,
creatorId: Long, creatorId: Long,
@@ -36,14 +56,10 @@ class ExplorerRepository(
) )
fun modifyCheers( fun modifyCheers(
cheersId: Long, request: PutModifyCheersRequest,
content: String,
token: String token: String
) = api.modifyCheers( ) = api.modifyCheers(
request = PutModifyCheersRequest( request = request,
cheersId = cheersId,
content = content
),
authHeader = token authHeader = token
) )
@@ -63,4 +79,18 @@ class ExplorerRepository(
size = size, size = size,
authHeader = token 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.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
@@ -10,7 +11,8 @@ import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemExplorerSectionBinding import kr.co.vividnext.sodalive.databinding.ItemExplorerSectionBinding
class ExplorerSectionAdapter( class ExplorerSectionAdapter(
private val onClickItem: (Long) -> Unit private val onClickItem: (Long) -> Unit,
private val isVisibleRanking: Boolean
) : RecyclerView.Adapter<ExplorerSectionAdapter.ViewHolder>() { ) : RecyclerView.Adapter<ExplorerSectionAdapter.ViewHolder>() {
private val items = mutableListOf<GetExplorerSectionCreatorResponse>() private val items = mutableListOf<GetExplorerSectionCreatorResponse>()
@@ -18,17 +20,50 @@ class ExplorerSectionAdapter(
inner class ViewHolder( inner class ViewHolder(
private val binding: ItemExplorerSectionBinding private val binding: ItemExplorerSectionBinding
) : RecyclerView.ViewHolder(binding.root) { ) : 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.tvNickname.text = item.nickname
binding.tvTags.text = item.tags binding.tvTags.text = item.tags
binding.ivProfile.load(item.profileImageUrl) { binding.ivProfile.load(item.profileImageUrl) {
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
crossfade(true) 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) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position]) holder.bind(items[position], index = position)
} }
override fun getItemCount() = items.size override fun getItemCount() = items.size

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,12 @@ import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R 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.BaseActivity
import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
@@ -56,6 +61,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
private lateinit var imm: InputMethodManager private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog private lateinit var loadingDialog: LoadingDialog
private lateinit var liveAdapter: UserProfileLiveAdapter private lateinit var liveAdapter: UserProfileLiveAdapter
private lateinit var audioContentAdapter: AudioContentAdapter
private lateinit var donationAdapter: UserProfileDonationAdapter private lateinit var donationAdapter: UserProfileDonationAdapter
private lateinit var similarCreatorAdapter: UserProfileSimilarCreatorAdapter private lateinit var similarCreatorAdapter: UserProfileSimilarCreatorAdapter
private lateinit var cheersAdapter: UserProfileCheersAdapter private lateinit var cheersAdapter: UserProfileCheersAdapter
@@ -118,6 +124,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
setupDonationView() setupDonationView()
setupSimilarCreatorView() setupSimilarCreatorView()
setupFanTalkView() setupFanTalkView()
setupAudioContentListView()
} }
private fun hideKeyboard(onAfterExecute: () -> Unit) { 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( rvCheers.layoutManager = LinearLayoutManager(
@@ -432,6 +466,52 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
rvCheers.adapter = cheersAdapter 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) { private fun showCheersReportPopup(cheersId: Long) {
val dialog = CheersReportDialog(this, layoutInflater) { val dialog = CheersReportDialog(this, layoutInflater) {
if (it.isBlank()) { if (it.isBlank()) {
@@ -477,6 +557,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
setCheers(it.cheers) setCheers(it.cheers)
setCreatorProfile(it.creator) setCreatorProfile(it.creator)
setCreatorNotice(it.notice, it.creator.creatorId) setCreatorNotice(it.notice, it.creator.creatorId)
setAudioContentList(it.contentList)
setLiveRoomList(it.liveRoomList) setLiveRoomList(it.liveRoomList)
setSimilarCreatorList(it.similarCreatorList) setSimilarCreatorList(it.similarCreatorList)
setUserDonationRanking(it.userDonationRanking) setUserDonationRanking(it.userDonationRanking)
@@ -536,7 +617,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
layoutUserProfile.ivProfile.load(creator.profileUrl) { layoutUserProfile.ivProfile.load(creator.profileUrl) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation()) 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") @SuppressLint("NotifyDataSetChanged")
private fun setLiveRoomList(liveRoomList: List<LiveRoomResponse>) { private fun setLiveRoomList(liveRoomList: List<LiveRoomResponse>) {
if (liveRoomList.isEmpty()) { if (liveRoomList.isEmpty()) {

View File

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

View File

@@ -20,7 +20,7 @@ class UserProfileSimilarCreatorAdapter(
fun bind(item: SimilarCreatorResponse) { fun bind(item: SimilarCreatorResponse) {
binding.ivProfile.load(item.profileImage) { binding.ivProfile.load(item.profileImage) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation()) 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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.ExplorerRepository 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.ReportRepository
import kr.co.vividnext.sodalive.report.ReportRequest import kr.co.vividnext.sodalive.report.ReportRequest
import kr.co.vividnext.sodalive.report.ReportType import kr.co.vividnext.sodalive.report.ReportType
@@ -256,18 +257,37 @@ class UserProfileViewModel(
) )
} }
fun modifyCheers(cheersId: Long, creatorId: Long, cheersContent: String) { fun modifyCheers(
if (cheersContent.isBlank()) { 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("내용을 입력하세요") _toastLiveData.postValue("내용을 입력하세요")
return return
} }
_isLoading.value = true _isLoading.value = true
val request = PutModifyCheersRequest(cheersId = cheersId)
if (cheersContent != null) {
request.content = cheersContent
}
if (isActive != null) {
request.isActive = isActive
}
compositeDisposable.add( compositeDisposable.add(
repository.modifyCheers( repository.modifyCheers(
cheersId = cheersId, request = request,
content = cheersContent,
token = "Bearer ${SharedPreferenceManager.token}" token = "Bearer ${SharedPreferenceManager.token}"
) )
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@@ -295,16 +315,16 @@ class UserProfileViewModel(
fun shareChannel(userId: Long, onSuccess: (String) -> Unit) { fun shareChannel(userId: Long, onSuccess: (String) -> Unit) {
_isLoading.value = true _isLoading.value = true
Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) { Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) {
link = Uri.parse("https://yozm.day/?channel_id=$userId") link = Uri.parse("https://sodalive.net/?channel_id=$userId")
domainUriPrefix = "https://yozm.page.link" domainUriPrefix = "https://sodalive.page.link"
androidParameters { } androidParameters { }
iosParameters("kr.co.vividnext.yozm") { iosParameters("kr.co.vividnext.sodalive") {
appStoreId = "1630284226" appStoreId = "6461721697"
} }
}.addOnSuccessListener { }.addOnSuccessListener {
val uri = it.shortLink val uri = it.shortLink
if (uri != null) { if (uri != null) {
onSuccess("요즘라이브 ${creatorNickname}님의 채널입니다.\n$uri") onSuccess("소다라이브 ${creatorNickname}님의 채널입니다.\n$uri")
} }
}.addOnFailureListener { }.addOnFailureListener {
_toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.") _toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.")

View File

@@ -4,5 +4,6 @@ import com.google.gson.annotations.SerializedName
data class PutModifyCheersRequest( data class PutModifyCheersRequest(
@SerializedName("cheersId") val cheersId: Long, @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( class UserProfileCheersAdapter(
private val userId: Long, private val userId: Long,
private val enterReply: (Long, String) -> Unit, private val enterReply: (Long, String) -> Unit,
private val modifyReply: (Long, String) -> Unit, private val modifyReply: (Long, String?) -> Unit,
private val onClickReport: (Long) -> Unit private val modifyCheers: (Long, String) -> Unit,
private val onClickReport: (Long) -> Unit,
private val onClickDelete: (Long) -> Unit
) : RecyclerView.Adapter<UserProfileCheersAdapter.ViewHolder>() { ) : RecyclerView.Adapter<UserProfileCheersAdapter.ViewHolder>() {
val items = mutableListOf<GetCheersResponseItem>() val items = mutableListOf<GetCheersResponseItem>()
@@ -35,19 +37,40 @@ class UserProfileCheersAdapter(
binding.ivProfile.load(cheers.profileUrl) { binding.ivProfile.load(cheers.profileUrl) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(16.7f.dpToPx())) transformations(RoundedCornersTransformation(16.7f.dpToPx()))
} }
binding.tvContent.text = cheers.content binding.tvContent.text = cheers.content
binding.tvNickname.text = cheers.nickname binding.tvNickname.text = cheers.nickname
binding.tvDate.text = cheers.date binding.tvDate.text = cheers.date
binding.ivMenu.setOnClickListener { if (
showOptionMenu( cheers.memberId == SharedPreferenceManager.userId ||
context, userId == SharedPreferenceManager.userId
binding.ivMenu, ) {
cheersId = cheers.cheersId 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()) { if (cheers.replyList.isNotEmpty()) {
@@ -106,16 +129,38 @@ class UserProfileCheersAdapter(
override fun getItemCount() = items.count() 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 popup = PopupMenu(context, v)
val inflater = popup.menuInflater 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 { popup.setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.menu_review_report -> { R.id.menu_review_report -> {
onClickReport(cheersId) onClickReport(cheersId)
} }
R.id.menu_review_modify -> {
onClickModify()
}
R.id.menu_review_delete -> {
onClickDelete(cheersId)
}
} }
true 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) { binding.ivProfile.load(item.profileImage) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation()) 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 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.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.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>( class UserProfileDonationAllViewActivity : BaseActivity<ActivityUserProfileLiveAllBinding>(
ActivityUserProfileLiveAllBinding::inflate 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 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.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.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>( class UserProfileFantalkAllViewActivity : BaseActivity<ActivityUserProfileFantalkAllBinding>(
ActivityUserProfileFantalkAllBinding::inflate 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.tvNickname.text = item.nickname
binding.ivProfile.load(item.profileImage) { binding.ivProfile.load(item.profileImage) {
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
crossfade(true) crossfade(true)
} }

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.View import android.view.View
import android.webkit.URLUtil
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
@@ -22,7 +21,6 @@ import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService 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.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog 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.LivePaymentDialog
import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog
import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditActivity 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 kr.co.vividnext.sodalive.settings.notification.MemberRole
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -95,25 +94,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
message = "라이브를 불러오고 있습니다." message = "라이브를 불러오고 있습니다."
viewModel.getSummary() 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() { private fun setupView() {
@@ -144,17 +124,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
} }
binding.swipeRefreshLayout.setOnRefreshListener { refreshSummary() } 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() { private fun refreshSummary() {
@@ -186,7 +155,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
.layoutParams = layoutParams .layoutParams = layoutParams
binding.layoutRecommendLive.pager.apply { binding.layoutRecommendLive.pager.apply {
adapter = RecommendLiveAdapter(pagerWidth.roundToInt(), pagerHeight) { adapter = RecommendLiveAdapter(requireContext(), pagerWidth.roundToInt(), pagerHeight) {
startActivity( startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply { Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it) putExtra(Constants.EXTRA_USER_ID, it)
@@ -373,11 +342,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
recyclerView.visibility = View.GONE recyclerView.visibility = View.GONE
binding.layoutLiveNow.tvAllView.visibility = View.GONE binding.layoutLiveNow.tvAllView.visibility = View.GONE
binding.layoutLiveNow.llNoItems.visibility = View.VISIBLE binding.layoutLiveNow.llNoItems.visibility = View.VISIBLE
binding.layoutLiveNow.tvMakeRoom.setOnClickListener {
val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
activityResultLauncher.launch(intent)
}
recyclerView.requestLayout() recyclerView.requestLayout()
binding.layoutLiveNow.llNoItems.requestLayout() binding.layoutLiveNow.llNoItems.requestLayout()
} else { } else {
@@ -460,12 +424,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
recyclerView.visibility = View.GONE recyclerView.visibility = View.GONE
binding.layoutLiveReservation.tvAllView.visibility = View.GONE binding.layoutLiveReservation.tvAllView.visibility = View.GONE
binding.layoutLiveReservation.llNoItems.visibility = View.VISIBLE 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() recyclerView.requestLayout()
binding.layoutLiveReservation.llNoItems.requestLayout() binding.layoutLiveReservation.llNoItems.requestLayout()
} else { } else {
@@ -496,7 +454,20 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
binding.eventBannerSlider.layoutParams = imageSliderLp binding.eventBannerSlider.layoutParams = imageSliderLp
binding.eventBannerSlider.apply { 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) setLifecycleRegistry(lifecycle)
setScrollDuration(800) setScrollDuration(800)
}.create() }.create()

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,18 @@
package kr.co.vividnext.sodalive.live.reservation package kr.co.vividnext.sodalive.live.reservation
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load import com.bumptech.glide.Glide
import coil.transform.RoundedCornersTransformation import com.bumptech.glide.load.resource.bitmap.CenterCrop
import kr.co.vividnext.sodalive.R 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.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ItemLiveReservationBinding import kr.co.vividnext.sodalive.databinding.ItemLiveReservationBinding
import kr.co.vividnext.sodalive.databinding.ItemMyLiveReservationBinding import kr.co.vividnext.sodalive.databinding.ItemMyLiveReservationBinding
@@ -24,6 +29,7 @@ class LiveReservationAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == 1) { return if (viewType == 1) {
MyLiveViewHolder( MyLiveViewHolder(
parent.context,
ItemMyLiveReservationBinding.inflate( ItemMyLiveReservationBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
@@ -32,6 +38,7 @@ class LiveReservationAdapter(
) )
} else { } else {
ViewHolder( ViewHolder(
parent.context,
ItemLiveReservationBinding.inflate( ItemLiveReservationBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
@@ -61,7 +68,7 @@ class LiveReservationAdapter(
} }
private fun isMyLive(item: GetRoomListResponse) = private fun isMyLive(item: GetRoomListResponse) =
item.managerId == SharedPreferenceManager.userId && isMain item.creatorId == SharedPreferenceManager.userId && isMain
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun clear() { fun clear() {
@@ -70,17 +77,34 @@ class LiveReservationAdapter(
} }
inner class ViewHolder( inner class ViewHolder(
private val context: Context,
private val binding: ItemLiveReservationBinding private val binding: ItemLiveReservationBinding
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetRoomListResponse) { fun bind(item: GetRoomListResponse) {
binding.ivCover.load(item.coverImageUrl) { val lp = binding.ivCover.layoutParams
crossfade(true) lp.width = 80f.dpToPx().toInt()
placeholder(R.drawable.ic_logo) lp.height = 116.7f.dpToPx().toInt()
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
} 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.tvDate.text = item.beginDateTime
binding.tvNickname.text = item.managerNickname binding.tvNickname.text = item.creatorNickname
binding.tvTitle.text = item.title binding.tvTitle.text = item.title
binding.root.setOnClickListener { onClick(item) } binding.root.setOnClickListener { onClick(item) }
binding.ivLock.visibility = if (item.isPrivateRoom) { binding.ivLock.visibility = if (item.isPrivateRoom) {
@@ -102,16 +126,11 @@ class LiveReservationAdapter(
"${item.price.moneyFormat()}" "${item.price.moneyFormat()}"
} }
} }
binding.iv19.visibility = if (item.isAdult) {
View.VISIBLE
} else {
View.GONE
}
} }
} }
inner class MyLiveViewHolder( inner class MyLiveViewHolder(
private val context: Context,
private val binding: ItemMyLiveReservationBinding private val binding: ItemMyLiveReservationBinding
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetRoomListResponse, position: Int) { fun bind(item: GetRoomListResponse, position: Int) {
@@ -120,13 +139,30 @@ class LiveReservationAdapter(
} else { } else {
View.GONE View.GONE
} }
binding.ivCover.load(item.coverImageUrl) {
crossfade(true) val lp = binding.ivCover.layoutParams
placeholder(R.drawable.ic_logo) lp.width = 80f.dpToPx().toInt()
transformations(RoundedCornersTransformation(4f.dpToPx())) 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.tvDate.text = item.beginDateTime
binding.tvNickname.text = item.managerNickname binding.tvNickname.text = item.creatorNickname
binding.tvTitle.text = item.title binding.tvTitle.text = item.title
binding.root.setOnClickListener { onClick(item) } binding.root.setOnClickListener { onClick(item) }
@@ -135,12 +171,6 @@ class LiveReservationAdapter(
} else { } else {
View.GONE 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("price") val price: String,
@SerializedName("haveCan") val haveCan: Int, @SerializedName("haveCan") val haveCan: Int,
@SerializedName("useCan") val useCan: Int, @SerializedName("useCan") val useCan: Int,
@SerializedName("remainingCoin") val remainingCoin: Int @SerializedName("remainingCan") val remainingCan: Int
) : Parcelable ) : Parcelable

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.CountDownTimer
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.Spannable import android.text.Spannable
@@ -106,7 +107,57 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
private var isMicrophoneMute = false private var isMicrophoneMute = false
private var isSpeaker = false private var isSpeaker = false
private var isSpeakerFold = 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) { private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@@ -151,6 +202,17 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
viewModel.getMemberCan() viewModel.getMemberCan()
viewModel.getRoomInfo(roomId) viewModel.getRoomInfo(roomId)
binding.etChat.setOnFocusChangeListener { view, hasFocus ->
if (isNoChatting && hasFocus) {
Toast.makeText(
applicationContext,
"${remainingNoChattingTime}초 동안 채팅하실 수 없습니다",
Toast.LENGTH_SHORT
).show()
view.clearFocus()
}
}
} }
override fun onStart() { override fun onStart() {
@@ -210,7 +272,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
) )
}, },
onClickInviteSpeaker = { memberId -> onClickInviteSpeaker = { memberId ->
if (speakerListAdapter.itemCount <= 9) { if (speakerListAdapter.itemCount <= 4) {
inviteSpeaker(memberId) inviteSpeaker(memberId)
} else { } else {
showToast("스피커 정원이 초과했습니다.") showToast("스피커 정원이 초과했습니다.")
@@ -233,6 +295,15 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
changeListenerMessage(memberId) changeListenerMessage(memberId)
}, },
onClickNoChatting = { userId, nickname, profileUrl ->
LiveRoomNoChattingDialog(
activity = this,
layoutInflater = layoutInflater,
nickname = nickname,
profileUrl = profileUrl,
confirmButtonClick = { setNoChatting(userId, nickname) }
).show(screenWidth)
},
onClickKickOut = { onClickKickOut = {
LiveDialog( LiveDialog(
activity = this, activity = this,
@@ -286,7 +357,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
) )
}, },
onClickInviteSpeaker = { onClickInviteSpeaker = {
if (speakerListAdapter.itemCount <= 9) { if (speakerListAdapter.itemCount <= 4) {
inviteSpeaker(it) inviteSpeaker(it)
} else { } else {
showToast("스피커 정원이 초과했습니다.") showToast("스피커 정원이 초과했습니다.")
@@ -305,6 +376,15 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
cancelButtonClick = {} cancelButtonClick = {}
).show(screenWidth) ).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 -> onClickPopupMenu = { userId, nickname, isBlock, view ->
showOptionMenu( showOptionMenu(
this, this,
@@ -399,6 +479,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
agora.deInitAgoraEngine() agora.deInitAgoraEngine()
} }
} }
countDownTimer.cancel()
super.onDestroy() super.onDestroy()
} }
@@ -559,21 +640,27 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} }
viewModel.roomInfoLiveData.observe(this) { response -> viewModel.roomInfoLiveData.observe(this) { response ->
binding.tvTitle.text = response.title binding.tv19.visibility = if (response.isAdult) {
binding.ivCover.load(response.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
}
isAvailableDonation = response.isAvailableDonation
binding.flDonation.visibility = if (response.isAvailableDonation) {
View.VISIBLE View.VISIBLE
} else { } else {
View.GONE 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 ( if (
response.managerId == SharedPreferenceManager.userId && response.creatorId == SharedPreferenceManager.userId &&
SharedPreferenceManager.role == MemberRole.CREATOR.name SharedPreferenceManager.role == MemberRole.CREATOR.name
) { ) {
binding.flDonationMessageList.visibility = View.VISIBLE binding.flDonationMessageList.visibility = View.VISIBLE
@@ -605,10 +692,10 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
binding.flDonationMessageList.visibility = View.GONE binding.flDonationMessageList.visibility = View.GONE
} }
speakerListAdapter.managerId = response.managerId speakerListAdapter.managerId = response.creatorId
speakerListAdapter.updateList(response.speakerList) speakerListAdapter.updateList(response.speakerList)
if (response.managerId == SharedPreferenceManager.userId) { if (response.creatorId == SharedPreferenceManager.userId) {
binding.ivEdit.setOnClickListener { binding.ivEdit.setOnClickListener {
roomInfoEditDialog.setRoomInfo(response.title, response.notice) roomInfoEditDialog.setRoomInfo(response.title, response.notice)
roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl) roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl)
@@ -625,7 +712,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
if (newCoverImageUri != null) { if (newCoverImageUri != null) {
binding.ivCover.load(newCoverImageUri) { binding.ivCover.load(newCoverImageUri) {
crossfade(true) 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}" binding.tvParticipate.text = "${response.participantsCount}"
setNoticeAndClickableUrl(binding.tvNotice, response.notice) setNoticeAndClickableUrl(binding.tvNotice, response.notice)
binding.tvCreatorNickname.text = response.managerNickname binding.tvCreatorNickname.text = response.creatorNickname
binding.ivCreatorProfile.load(response.managerProfileUrl) { binding.ivCreatorProfile.load(response.creatorProfileUrl) {
crossfade(true) crossfade(true)
placeholder(R.drawable.ic_logo) placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
binding.ivCreatorProfile.setOnClickListener { binding.ivCreatorProfile.setOnClickListener {
if (response.managerId != SharedPreferenceManager.userId) { if (response.creatorId != SharedPreferenceManager.userId) {
showLiveRoomUserProfileDialog(userId = response.managerId) showLiveRoomUserProfileDialog(userId = response.creatorId)
} }
} }
if (response.isAvailableDonation) { if (response.creatorId != SharedPreferenceManager.userId) {
binding.ivCreatorFollow.visibility = View.VISIBLE binding.ivCreatorFollow.visibility = View.VISIBLE
if (response.isFollowing) {
if (response.isFollowingManager) {
binding.ivCreatorFollow.setImageResource(R.drawable.btn_following) binding.ivCreatorFollow.setImageResource(R.drawable.btn_following)
binding.ivCreatorFollow.setOnClickListener { binding.ivCreatorFollow.setOnClickListener {
viewModel.creatorUnFollow( viewModel.creatorUnFollow(
creatorId = response.managerId, creatorId = response.creatorId,
roomId = roomId roomId = roomId
) )
} }
@@ -704,7 +796,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
binding.ivCreatorFollow.setImageResource(R.drawable.btn_follow) binding.ivCreatorFollow.setImageResource(R.drawable.btn_follow)
binding.ivCreatorFollow.setOnClickListener { binding.ivCreatorFollow.setOnClickListener {
viewModel.creatorFollow( viewModel.creatorFollow(
creatorId = response.managerId, creatorId = response.creatorId,
roomId = roomId 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( private fun showDialog(
content: String, content: String,
cancelTitle: String = "", cancelTitle: String = "",
@@ -1004,7 +1107,13 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
val profileUrl = viewModel.getUserProfileUrl(SharedPreferenceManager.userId.toInt()) val profileUrl = viewModel.getUserProfileUrl(SharedPreferenceManager.userId.toInt())
val rank = viewModel.getUserRank(SharedPreferenceManager.userId) 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() val message = binding.etChat.text.toString()
chatAdapter.items.add( chatAdapter.items.add(
LiveRoomNormalChat( LiveRoomNormalChat(
@@ -1082,6 +1191,8 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} }
private fun joinChannel(roomInfo: GetRoomInfoResponse) { private fun joinChannel(roomInfo: GetRoomInfoResponse) {
loadingDialog.show(width = screenWidth, message = "라이브에 입장하고 있습니다.")
val userId = SharedPreferenceManager.userId val userId = SharedPreferenceManager.userId
agora.joinRtcChannel( agora.joinRtcChannel(
uid = userId.toInt(), uid = userId.toInt(),
@@ -1190,7 +1301,11 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} }
}, },
rtmChannelJoinSuccess = { rtmChannelJoinSuccess = {
if (userId == roomInfo.managerId) { handler.post {
loadingDialog.dismiss()
}
if (userId == roomInfo.creatorId) {
setBroadcaster() setBroadcaster()
} else { } else {
setAudience() setAudience()
@@ -1204,6 +1319,10 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} else { } else {
startService(intent) startService(intent)
} }
if (containNoChatRoom()) {
startNoChatting()
}
}, },
rtmChannelJoinFail = { rtmChannelJoinFail = {
agoraConnectFail() agoraConnectFail()
@@ -1212,8 +1331,11 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} }
private fun agoraConnectFail() { private fun agoraConnectFail() {
showToast("라이브에 접속하지 못했습니다.\n다시 시도해 주세요.") handler.post {
finish() loadingDialog.dismiss()
showToast("라이브에 접속하지 못했습니다.\n다시 시도해 주세요.")
finish()
}
} }
private val rtcEventHandler = object : IRtcEngineEventHandler() { private val rtcEventHandler = object : IRtcEngineEventHandler() {
@@ -1398,7 +1520,19 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
viewModel.getRoomInfo(roomId = roomId) viewModel.getRoomInfo(roomId = roomId)
return 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("KICK_OUT") KICK_OUT,
@SerializedName("SET_MANAGER") SET_MANAGER, @SerializedName("SET_MANAGER") SET_MANAGER,
@SerializedName("RELEASE_MANAGER") RELEASE_MANAGER, @SerializedName("RELEASE_MANAGER") RELEASE_MANAGER,
@SerializedName("NO_CHATTING") NO_CHATTING,
} }

View File

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

View File

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

View File

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

View File

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

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)) alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
if (can > 0) { if (can > 0) {
dialogView.tvCoin.visibility = View.VISIBLE dialogView.tvCan.visibility = View.VISIBLE
dialogView.tvCoin.text = can.moneyFormat() dialogView.tvCan.text = can.moneyFormat()
dialogView.tvConfirm.text = "으로 입장" dialogView.tvConfirm.text = "으로 입장"
} else { } else {
dialogView.tvCoin.visibility = View.GONE dialogView.tvCan.visibility = View.GONE
dialogView.tvConfirm.text = "입장하기" dialogView.tvConfirm.text = "입장하기"
} }

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