Compare commits

...

95 Commits

Author SHA1 Message Date
klaus 6fbb98ca7b 유료방 입장 팝업
- UI 수정
- 알림 문구 수정
2024-01-22 16:09:53 +09:00
klaus b33c8ffd6d 유료방 입장 팝업
- 라이브 시작 시각 -> 시작 시각 으로 변경
2024-01-22 03:19:12 +09:00
klaus 14b3bfbae7 유료방 입장 팝업
- 1시간 이상 지난 후 팝업 내용 변경
- 라이브 시작 시각, 현재 시각 표시
2024-01-21 17:34:07 +09:00
klaus 7386c93d73 커뮤니티 글 - 줄 간격 0 -> 8 로 변경 2024-01-19 16:16:47 +09:00
klaus 4af8d7dce1 라이브 - 배경이미지 가운데 정렬 2024-01-18 18:46:59 +09:00
klaus 08d2ccdab4 라이브 - 배경이미지 상단정렬 2024-01-18 18:27:06 +09:00
klaus 0a8abeefbe 앱이 켜져 있을 때 딥링크가 동작하지 않던 버그 수정 2024-01-17 03:53:41 +09:00
klaus f90e089f8a 프로필 이미지 변경
- 이미지 변경시 로컬에 저장된 프로필 이미지 URL도 변경 하도록 수정
2024-01-17 03:14:04 +09:00
klaus 1eb905fe30 라이브
- 상단 총 받은 캔 사이즈 10->14로 변경
2024-01-16 22:44:14 +09:00
klaus fcc55288b1 라이브
- 오른쪽 하단 버튼 패딩 수정
2024-01-16 22:37:03 +09:00
klaus 97c5dc4363 라이브
- 9970ff 색상 3bb9f1로 변경
2024-01-16 22:18:05 +09:00
klaus 4223f0bc5d 라이브
- 후원, 입장, 룰렛 결과 채팅 배경색 변경
2024-01-16 21:34:48 +09:00
klaus 7ada4515aa 라이브 - 공지 활성화/비활성화 버튼 색 변경 2024-01-16 19:38:46 +09:00
klaus 8456f2b30b 라이브 - 19 배경 변경 2024-01-16 18:56:47 +09:00
klaus c20802f89c 라이브 - 공지 UI 수정 2024-01-16 18:20:03 +09:00
klaus 804f993b25 라이브 - 아고라 로컬 사용자의 음성 활동 감지를 비활성화 하여 말하는 유저 표시를 좀 더 자연스럽게 수정 2024-01-16 17:24:11 +09:00
klaus 869b83804d 크리에이터가 말할 때 크리에이터 배경 표시 2024-01-16 15:59:34 +09:00
klaus 9d97feb184 라이브 공지 버튼 수정 2024-01-16 15:53:51 +09:00
klaus 42a69609a1 라이브 배경 이미지 사이즈 비율
400:564로 변경
2024-01-16 15:10:15 +09:00
klaus 3a541f71a6 라이브 상단 UI 변경 2024-01-16 05:31:23 +09:00
klaus d75e4af348 댓글 쓰기 텍스트 필드 border 색상 변경 2024-01-15 23:38:30 +09:00
klaus 2dac54b3ec 크리에이터 커뮤니티
- 링크 적용
2024-01-15 23:36:02 +09:00
klaus 82cf1658cb 라이브 방
- 말하는 사람 배경 변경
- 배경 On/Off 색상 변경
- 참여자 숫자 색상 변경, 폰트 Bold
2024-01-15 23:11:08 +09:00
klaus 127e1f6673 본인이 스피커 인 경우 말하는 사람 배경이 잘 보이지 않는 버그 수정 2024-01-15 21:05:30 +09:00
klaus 22f1e4024e versionName 1.4.0, versionCode 18 2024-01-11 16:04:51 +09:00
klaus b0bfe2ac06 콘텐츠 상세
- 오픈 예정 날짜 데이터가 있더라도 콘텐츠를 올린 크리에이터의 경우 재생버튼이 동작하도록 수정
2024-01-11 00:27:45 +09:00
klaus d67bb8be50 크리에이터 채널, 콘텐츠 상세
- 오픈예정 추가
2024-01-10 01:57:04 +09:00
klaus eeac702cd7 다이얼로그
- 취소 버튼 글자색 변경
2024-01-09 19:46:37 +09:00
klaus c2216ab054 라이브 예약완료
- 홈으로 이동, 예약이 완료되었습니다 string 글자색 변경
2024-01-08 20:23:36 +09:00
klaus 45e5494653 콘텐츠 업로드
- 예약 업로드를 위해 날짜/시간 선택 추가
2024-01-08 15:10:30 +09:00
klaus 5f7ed22711 라이브 만들기 액티비티
- 요즘라이브 컬러에서 소다라이브 컬러로 변경
2024-01-05 22:15:07 +09:00
klaus 49a823895d 콘텐츠 업로드 액티비티
- 요즘라이브 컬러에서 소다라이브 컬러로 변경
2024-01-05 17:38:01 +09:00
klaus 92324e3409 커뮤니티
- 이미지가 잘려서 보이는 현상 수정
2024-01-04 15:41:05 +09:00
klaus 13057af98a 쿠폰번호 입력 필터 수정
AS-IS : 영대문자와 숫자만 입력되도록 필터적용

TO-BE : 영문이 입력되면 대문자로 변경되지만 나머지 문자는 입력될 수 있도록 수정

- 기존의 InputFilter는 입력하다보면 앞에 문자가 반복해서 입력되는 기기도 있는 것 확인
2024-01-04 12:18:33 +09:00
klaus 144ff4af05 마이페이지 padding 추정 2024-01-03 15:00:22 +09:00
klaus 315e0627d1 쿠폰등록 페이지 추가 2024-01-03 05:37:21 +09:00
klaus 4bd6a766f5 메인 하단 탭, 마이페이지
- 글자 색 , 배경색 변경
2024-01-02 17:26:26 +09:00
klaus 32f99cc678 versionCode 17, versionName 1.3.3 2024-01-01 21:24:45 +09:00
klaus bd1800c2b5 라이브 - 커버이미지 수정방식 변경
AS-IS : 방 정보를 가져올 때 마다 변경
TO-BE : 이전 이미지 url과 다른 경우 에만 커버이미지 변경
2024-01-01 21:08:39 +09:00
klaus bf3e6230a0 라이브 채팅 폰트 사이즈
AS-IS : 14sp
TO-BE : 15sp
2023-12-27 00:23:01 +09:00
klaus 2400123a92 라이브 채팅 폰트 사이즈
AS-IS : 13sp
TO-BE : 14sp
2023-12-27 00:06:50 +09:00
klaus 525e399423 . 2023-12-26 22:25:05 +09:00
klaus 5da644a607 라이브 룰렛 색상 변경
AS-IS : 10가지 색상 순서 대로 표시
TO-BE : 10가지 색상 중 랜덤 으로 표시
2023-12-26 21:57:31 +09:00
klaus 3d231ea8be 라이브 채팅 폰트 변경
AS-IS : Gmarket Sans
TO-BE : System Font
2023-12-26 21:48:06 +09:00
klaus d9ad230804 라이브 룰렛 색상 수정
AS-IS : -
TO-BE : -
2023-12-26 19:18:44 +09:00
klaus 41a92a871d 스플래시 페이지 수정
AS-IS : 크리스마스 이미지
TO-BE : 신년 이미지
2023-12-26 17:41:28 +09:00
klaus 5634e0787f 룰렛 최대 개수 수정
AS-IS : 최대 6개
TO-BE : 최대 10개

룰렛 확률 수정
AS-IS : 소수점 없음
TO-BE : 소수점 2자리
2023-12-26 15:53:14 +09:00
klaus 8a7406aa22 versionCode 16, versionName 1.3.2 2023-12-26 00:31:22 +09:00
klaus ab24042863 커뮤니티 게시물 - 콘텐츠 글 더보기 기능 추가 2023-12-25 23:51:02 +09:00
klaus 5d3891c1db 게시물이 없을 때 Write 버튼이 눌리지 않는 버그 수정
versionCode 15, versionName 1.3.1
2023-12-25 20:06:51 +09:00
klaus 7d9fc13192 versionCode 14, versionName 1.3.0 2023-12-25 18:03:11 +09:00
klaus 12f944c79d 라이브 중 전체보기 - 불필요한 아바타 아이콘 제거 2023-12-25 17:03:36 +09:00
klaus e0da65e64f 커뮤니티 등록/수정 - 이미지 등록 알림 메시지 2줄로 조정 2023-12-25 16:49:13 +09:00
klaus 857b4de792 커뮤니티 수정 추가 2023-12-25 09:29:36 +09:00
klaus c896be5ece 라이브 메인 - 커뮤니티 클릭 이벤트 추가 2023-12-25 07:44:17 +09:00
klaus 6c96c4afe5 커뮤니티 신고하기 추가 2023-12-25 05:42:27 +09:00
klaus 6b2e59c09d 커뮤니티 게시물 삭제 추가 2023-12-25 05:15:50 +09:00
klaus 481cad1a46 커뮤니티 게시물 업로드 페이지 추가 2023-12-25 04:09:25 +09:00
klaus 62ecf3dd51 커뮤니티 전체보기 페이지 추가 2023-12-25 02:13:57 +09:00
klaus 116dde1b7e 메인 라이브 탭 - 커뮤니티 포스트 영역 추가 2023-12-23 00:03:03 +09:00
klaus 5db097d67c 크리에이터 채널 - 커뮤니티 영역 추가 2023-12-22 19:48:05 +09:00
klaus fbcdbf3b48 크리에이터 채널 - 공지사항 영역 제거 2023-12-22 16:24:10 +09:00
klaus 479f956db3 크리에이터 채널 - 함께 들으면 좋은 채널 제거 2023-12-22 16:18:17 +09:00
klaus 72d16c4d18 지금 라이브 중 - 라이브 없을 떄 문구 변경, 참여자 숫자 제거 2023-12-22 16:09:35 +09:00
klaus 2984aac0a5 라이브 탭 - 라이브 중 아이템 UI 변경 2023-12-14 18:27:58 +09:00
klaus 8ddf85c1be 콘텐츠 메인 API 분리 및 속도 개선 2023-12-13 15:58:31 +09:00
klaus 8f3a2f16ad versionCode 13, versionName 1.2.1 2023-12-08 23:54:13 +09:00
klaus 6a72bc63c0 라이브 배경이미지 캐시 적용 2023-12-08 23:45:47 +09:00
klaus f74d8bfc16 스플래시 - 크리스마스 로고 변경 2023-12-07 12:56:49 +09:00
klaus 29e293e6b5 룰렛 설정 - 설정된 적이 없을 때 기본 캔 설정 5 -> 0으로 변경 2023-12-07 11:06:21 +09:00
klaus 2883c63e07 versionCode 12, versionName 1.2.0 2023-12-07 09:59:47 +09:00
klaus dfaf45a83d 스플래시 - 크리스마스 버전으로 변경 2023-12-07 09:59:24 +09:00
klaus c2b99552da 라이브 방 룰렛 - 룰렛 결과 전송 데이터 수정, 룰렛 설정 완료 메시지 수정 2023-12-07 04:18:42 +09:00
klaus e4a92e0f2b 라이브 방 - 커버이미지 캐시 적용 2023-12-06 16:55:18 +09:00
klaus dabf8563b8 텍스트 메시지 쓰기 - 메시지 영역 debounce timeout 500 -> 100ms로 조정 2023-12-05 14:19:16 +09:00
klaus fb9c138eb4 라이브 방 룰렛 - 컬러 변경 2023-12-04 22:25:16 +09:00
klaus 7da0e2509c 라이브 방 룰렛 설정 - 당첨 내역 제거 2023-12-04 20:57:52 +09:00
klaus 63c2d607cc 라이브 방 룰렛 - 룰렛판 추가 2023-12-04 15:17:05 +09:00
klaus 9f66cb91fc 라이브 방 룰렛 - 룰렛 돌리기 기능 추가 2023-12-02 05:11:55 +09:00
klaus 91db6caec9 라이브 방 룰렛
- 리스너용 룰렛 버튼 추가
- 룰렛 설정 시 룰렛 버튼 토글 메시지를 발송하여 리스너 화면에서 룰렛이 (비)활성화 되도록 수정
2023-12-02 00:23:36 +09:00
klaus 4a4940f04d 라이브 방 룰렛 설정 - 미리보기 다이얼로그 추가 2023-12-01 16:49:07 +09:00
klaus 952df91619 라이브 방 - 룰렛 설정 추가 2023-12-01 03:37:23 +09:00
klaus b359ca58ba 라이브 방 - 배경 이미지 로딩 시 placeholder 제거 2023-11-24 21:49:01 +09:00
klaus fe121ee89b 라이브 프로필 다이얼로그
- 크리에이터가 아닌 사람 태그 영역 숨김
2023-11-23 23:44:51 +09:00
klaus 9e3859e2c2 콘텐츠 전체 보기
- 콘텐츠 상세로 이동 후 복귀 해도 콘텐츠 삭제/수정을 하지 않았을 때 스크롤이 상단으로 이동하지 않도록 수정
2023-11-23 00:19:50 +09:00
klaus f4ca63af9d 라이브 유저 프로필 다이얼로그
- 크리에이터가 아닌 일반 유저는 태그 영역과 소개 영역이 보이지 않도록 제거
2023-11-22 23:51:26 +09:00
klaus 61ee1fc5be version
- code : 11
- name : 1.1.1
2023-11-22 01:38:27 +09:00
klaus ead7aa3011 라이브 - 방장이 아닌 사용자가 유저 차단 시 라이브에서 강퇴되는 버그 수정 2023-11-21 14:19:40 +09:00
klaus 720df22df5 무료 충전 제거 2023-11-21 00:23:40 +09:00
klaus 74c56c2061 foreground service type 추가 - mediaPlayback 2023-11-20 22:44:18 +09:00
klaus e391fcbb6e 프로필 수정 페이지 - sns, 소개, 태그 수정 UI 크리에이터만 보이도록 수정 2023-11-20 17:56:52 +09:00
klaus 87e1156b02 콘텐츠 후원 배경색 - 기존 캔 수 / 10 으로 조정 2023-11-20 15:41:16 +09:00
klaus a77708dfbf 라이브 후원 배경색 - 기존 캔 수 / 10 으로 조정 2023-11-20 15:21:39 +09:00
klaus 664f34ed5b 라이브 프로필 다이얼로그
- 메시지 보내기 버튼 제거
2023-11-20 11:40:57 +09:00
klaus 48d1db3b79 라이브 상세 - 날짜 포맷 yyyy년 MM월 dd일 (E) a hh시 mm분 로 수정 2023-11-14 12:37:28 +09:00
270 changed files with 9679 additions and 1912 deletions

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
<State />
</entry>
</value>
</component>
</project>

10
.idea/migrations.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

View File

@ -40,8 +40,8 @@ android {
applicationId "kr.co.vividnext.sodalive"
minSdk 23
targetSdk 33
versionCode 10
versionName "1.1.0"
versionCode 19
versionName "1.4.1"
}
buildTypes {
@ -138,7 +138,7 @@ dependencies {
implementation "io.github.bootpay:android:4.3.4"
// agora
implementation "io.agora.rtc:voice-sdk:4.1.0-1"
implementation "io.agora.rtc:voice-sdk:4.2.6"
implementation 'io.agora.rtm:rtm-sdk:1.5.3'
// sound visualizer
@ -149,7 +149,4 @@ dependencies {
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
implementation "com.michalsvec:single-row-calednar:1.0.0"
// PointClick Maven Remote Repo
implementation 'kr.co.pointclick.sdk.offerwall:pointclick-sdk-offerwall:1.0.17'
}

View File

@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
@ -85,6 +86,7 @@
<activity android:name=".mypage.can.status.CanStatusActivity" />
<activity android:name=".mypage.can.charge.CanChargeActivity" />
<activity android:name=".mypage.can.payment.CanPaymentActivity" />
<activity android:name=".mypage.can.coupon.CanCouponActivity" />
<activity android:name=".live.room.create.LiveRoomCreateActivity" />
<activity android:name=".live.room.update.LiveRoomEditActivity" />
<activity android:name=".live.reservation.complete.LiveReservationCompleteActivity" />
@ -94,8 +96,10 @@
<activity android:name=".explorer.profile.UserProfileActivity" />
<activity android:name=".explorer.profile.donation.UserProfileDonationAllViewActivity" />
<activity android:name=".explorer.profile.fantalk.UserProfileFantalkAllViewActivity" />
<activity android:name=".explorer.profile.CreatorNoticeWriteActivity" />
<activity android:name=".explorer.profile.follow.UserFollowerListActivity" />
<activity android:name=".explorer.profile.creator_community.all.CreatorCommunityAllActivity" />
<activity android:name=".explorer.profile.creator_community.write.CreatorCommunityWriteActivity" />
<activity android:name=".explorer.profile.creator_community.modify.CreatorCommunityModifyActivity" />
<activity android:name=".message.text.TextMessageWriteActivity" />
<activity android:name=".message.text.TextMessageDetailActivity" />
<activity android:name=".message.SelectMessageRecipientActivity" />
@ -124,6 +128,7 @@
<activity android:name=".audio_content.curation.AudioContentCurationActivity" />
<activity android:name=".audio_content.all.AudioContentNewAllActivity" />
<activity android:name=".audio_content.all.AudioContentRankingAllActivity" />
<activity android:name=".live.roulette.config.RouletteConfigActivity" />
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
@ -134,9 +139,13 @@
<service
android:name=".common.SodaLiveService"
android:foregroundServiceType="microphone|mediaPlayback"
android:stopWithTask="false" />
<service android:name=".audio_content.AudioContentPlayService" />
<service
android:name=".audio_content.AudioContentPlayService"
android:foregroundServiceType="mediaPlayback"
android:stopWithTask="false" />
<!-- [START firebase_service] -->
<service

View File

@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatDelegate
import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.di.AppDI
@ -26,6 +27,8 @@ class SodaLiveApp : Application() {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
SharedPreferenceManager.init(applicationContext)
ImageLoaderProvider.init(applicationContext)
}
private fun isDebuggable(): Boolean {

View File

@ -43,6 +43,7 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
) {
if (it.resultCode == Activity.RESULT_OK) {
viewModel.page = 1
viewModel.isLast = false
viewModel.getAudioContentList(userId = userId) { finish() }
}
}

View File

@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.audio_content
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
@ -33,6 +34,12 @@ class AudioContentAdapter(
binding.tvLikeCount.text = item.likeCount.moneyFormat()
binding.tvCommentCount.text = item.commentCount.moneyFormat()
binding.tvScheduledToOpen.visibility = if (item.isScheduledToOpen) {
View.VISIBLE
} else {
View.GONE
}
if (item.price < 1) {
binding.tvPrice.text = "무료"
binding.tvPrice.setCompoundDrawables(null, null, null, null)

View File

@ -10,9 +10,11 @@ import kr.co.vividnext.sodalive.audio_content.detail.GetAudioContentDetailRespon
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeResponse
import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationRequest
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentCurationResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRanking
import kr.co.vividnext.sodalive.audio_content.main.GetNewContentUploadCreator
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListResponse
import kr.co.vividnext.sodalive.audio_content.order.OrderRequest
import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse
@ -125,11 +127,6 @@ interface AudioContentApi {
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/main")
fun getMain(
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentMainResponse>>
@GET("/audio-content/main/new")
fun getNewContentOfTheme(
@Query("theme") theme: String,
@ -182,4 +179,26 @@ interface AudioContentApi {
@Query("sort-type") sortType: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentRanking>>
@GET("/audio-content/main/curation-list")
fun getCurationList(
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentCurationResponse>>>
@GET("/audio-content/main/new-content-upload-creator")
fun getNewContentUploadCreatorList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetNewContentUploadCreator>>>
@GET("/audio-content/main/banner-list")
fun getMainBannerList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentBannerResponse>>>
@GET("/audio-content/main/order-list")
fun getMainOrderList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
}

View File

@ -129,8 +129,6 @@ class AudioContentRepository(
token: String
) = api.likeContent(request, authHeader = token)
fun getMain(token: String) = api.getMain(authHeader = token)
fun getNewContentOfTheme(theme: String, token: String) = api.getNewContentOfTheme(
theme = theme,
authHeader = token
@ -177,4 +175,17 @@ class AudioContentRepository(
sortType = sortType,
authHeader = token
)
fun getCurationList(page: Int, size: Int, token: String) = api.getCurationList(
page = page - 1,
size = size,
authHeader = token
)
fun getNewContentUploadCreatorList(
token: String
) = api.getNewContentUploadCreatorList(authHeader = token)
fun getMainBannerList(token: String) = api.getMainBannerList(authHeader = token)
fun getMainOrderList(token: String) = api.getMainOrderList(authHeader = token)
}

View File

@ -39,7 +39,7 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
PRICE_LOW
}
private var isLast = false
var isLast = false
var page = 1
private val size = 10

View File

@ -9,7 +9,7 @@ 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.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog

View File

@ -9,7 +9,7 @@ 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.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog

View File

@ -44,27 +44,27 @@ class AudioContentCommentAdapter(
binding.tvDonationCan.text = can.moneyFormat()
binding.llDonationCan.setBackgroundResource(
when {
can >= 100000 -> {
can >= 10000 -> {
R.drawable.bg_round_corner_10_7_973a3a
}
can >= 50000 -> {
can >= 5000 -> {
R.drawable.bg_round_corner_10_7_d85e37
}
can >= 10000 -> {
can >= 1000 -> {
R.drawable.bg_round_corner_10_7_d38c38
}
can >= 5000 -> {
can >= 500 -> {
R.drawable.bg_round_corner_10_7_59548f
}
can >= 1000 -> {
can >= 100 -> {
R.drawable.bg_round_corner_10_7_4d6aa4
}
can >= 500 -> {
can >= 50 -> {
R.drawable.bg_round_corner_10_7_2d7390
}

View File

@ -124,27 +124,27 @@ class AudioContentCommentReplyHeaderViewHolder(
binding.tvDonationCan.text = can.moneyFormat()
binding.llDonationCan.setBackgroundResource(
when {
can >= 100000 -> {
can >= 10000 -> {
R.drawable.bg_round_corner_10_7_973a3a
}
can >= 50000 -> {
can >= 5000 -> {
R.drawable.bg_round_corner_10_7_d85e37
}
can >= 10000 -> {
can >= 1000 -> {
R.drawable.bg_round_corner_10_7_d38c38
}
can >= 5000 -> {
can >= 500 -> {
R.drawable.bg_round_corner_10_7_59548f
}
can >= 1000 -> {
can >= 100 -> {
R.drawable.bg_round_corner_10_7_4d6aa4
}
can >= 500 -> {
can >= 50 -> {
R.drawable.bg_round_corner_10_7_2d7390
}

View File

@ -14,6 +14,7 @@ import android.widget.RelativeLayout
import android.widget.SeekBar
import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
@ -60,10 +61,6 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
private var creatorId: Long = 0
private var refresh = false
set(value) {
field = value
setResult(RESULT_OK)
}
private var title = ""
@SuppressLint("SetTextI18n")
@ -97,7 +94,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
override fun onResume() {
super.onResume()
val intentFilter = IntentFilter(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
registerReceiver(audioContentReceiver, intentFilter)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(audioContentReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(audioContentReceiver, intentFilter)
}
if (refresh) {
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
@ -283,6 +284,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
when (it.itemId) {
R.id.menu_modify -> {
refresh = true
setResult(RESULT_OK)
startActivity(
Intent(applicationContext, AudioContentModifyActivity::class.java)
.apply {
@ -496,14 +498,30 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
private fun setupPurchaseButton(response: GetAudioContentDetailResponse) {
if (
if (response.releaseDate != null) {
binding.llPurchase.visibility = View.VISIBLE
binding.llPurchasePrice.visibility = View.GONE
binding.tvReleaseDate.visibility = View.VISIBLE
binding.llPurchase.background = ContextCompat.getDrawable(
applicationContext,
R.drawable.bg_round_corner_5_3_525252
)
binding.tvReleaseDate.text = response.releaseDate
} else if (
response.price > 0 &&
!response.existOrdered &&
response.orderType == null &&
response.creator.creatorId != SharedPreferenceManager.userId
) {
binding.tvReleaseDate.visibility = View.GONE
binding.llPurchase.visibility = View.VISIBLE
binding.llPurchasePrice.visibility = View.VISIBLE
binding.tvPrice.text = response.price.toString()
binding.llPurchase.background = ContextCompat.getDrawable(
applicationContext,
R.drawable.bg_round_corner_5_3_3bb9f1
)
binding.tvStrPurchaseOrRental.text = if (response.isOnlyRental) {
" 대여하기"
@ -529,22 +547,36 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
.apply(RequestOptions().override((screenWidth - 13.3f.dpToPx()).toInt()))
.into(binding.ivCover)
binding.ivPlayOrPause.setOnClickListener {
startService(
Intent(this, AudioContentPlayService::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL, response.coverImageUrl)
putExtra(Constants.EXTRA_AUDIO_CONTENT_URL, response.contentUrl)
putExtra(Constants.EXTRA_NICKNAME, response.creator.nickname)
putExtra(Constants.EXTRA_AUDIO_CONTENT_TITLE, response.title)
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, response.contentId)
putExtra(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, response.creator.creatorId)
putExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, response.price <= 0)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PREVIEW,
!response.existOrdered && response.price > 0
)
}
)
if (
response.releaseDate == null ||
response.creator.creatorId == SharedPreferenceManager.userId
) {
binding.ivPlayOrPause.visibility = View.VISIBLE
binding.ivPlayOrPause.setOnClickListener {
startService(
Intent(this, AudioContentPlayService::class.java).apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL,
response.coverImageUrl
)
putExtra(Constants.EXTRA_AUDIO_CONTENT_URL, response.contentUrl)
putExtra(Constants.EXTRA_NICKNAME, response.creator.nickname)
putExtra(Constants.EXTRA_AUDIO_CONTENT_TITLE, response.title)
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, response.contentId)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID,
response.creator.creatorId
)
putExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, response.price <= 0)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PREVIEW,
!response.existOrdered && response.price > 0
)
}
)
}
} else {
binding.ivPlayOrPause.visibility = View.GONE
}
binding.tvTotalDuration.text = " / ${response.duration}"
@ -552,6 +584,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
@SuppressLint("SetTextI18n")
private fun setupInfoArea(response: GetAudioContentDetailResponse) {
binding.tvScheduledToOpen.visibility = if (response.releaseDate != null) {
View.VISIBLE
} else {
View.GONE
}
binding.tvTheme.text = response.themeStr
binding.tv19.visibility = if (response.isAdult) {
View.VISIBLE

View File

@ -14,6 +14,7 @@ data class GetAudioContentDetailResponse(
@SerializedName("tag") val tag: String,
@SerializedName("price") val price: Int,
@SerializedName("duration") val duration: String,
@SerializedName("releaseDate") val releaseDate: String?,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isMosaic") val isMosaic: Boolean,
@SerializedName("isOnlyRental") val isOnlyRental: Boolean,

View File

@ -17,17 +17,26 @@ import androidx.recyclerview.widget.RecyclerView
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.pointclick.sdk.offerwall.core.PointClickAd
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllActivity
import kr.co.vividnext.sodalive.audio_content.all.AudioContentRankingAllActivity
import kr.co.vividnext.sodalive.audio_content.curation.AudioContentCurationActivity
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerViewModel
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationAdapter
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationViewModel
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentViewModel
import kr.co.vividnext.sodalive.audio_content.main.new_content_upload_creator.AudioContentMainNewContentCreatorAdapter
import kr.co.vividnext.sodalive.audio_content.main.new_content_upload_creator.AudioContentMainNewContentCreatorViewModel
import kr.co.vividnext.sodalive.audio_content.main.order.AudioContentMainOrderListViewModel
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingAdapter
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingViewModel
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
@ -40,32 +49,37 @@ import kotlin.math.roundToInt
class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
FragmentAudioContentMainBinding::inflate
) {
private val viewModel: AudioContentMainViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var imm: InputMethodManager
private val newContentCreatorViewModel: AudioContentMainNewContentCreatorViewModel by inject()
private lateinit var newContentCreatorAdapter: AudioContentMainNewContentCreatorAdapter
private val bannerViewModel: AudioContentMainBannerViewModel by inject()
private lateinit var bannerAdapter: AudioContentMainBannerAdapter
private val orderListViewModel: AudioContentMainOrderListViewModel by inject()
private lateinit var orderListAdapter: AudioContentMainContentAdapter
private val newContentViewModel: AudioContentMainNewContentViewModel by inject()
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentAdapter: AudioContentMainContentAdapter
private val contentRankingViewModel: AudioContentMainRankingViewModel by inject()
private lateinit var contentRankingSortAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var contentRankingAdapter: AudioContentMainRankingAdapter
private val curationViewModel: AudioContentMainCurationViewModel by inject()
private lateinit var curationAdapter: AudioContentMainCurationAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireContext().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
setupView()
bindData()
viewModel.getMain()
curationViewModel.getCurationList()
bannerViewModel.getMainBannerList()
newContentViewModel.getThemeList()
newContentViewModel.getNewContentOfTheme("전체")
contentRankingViewModel.getContentRanking()
contentRankingViewModel.getContentRankingSortType()
newContentCreatorViewModel.getNewContentUploadCreatorList()
}
private fun setupView() {
@ -94,11 +108,13 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
binding.swipeRefreshLayout.setOnRefreshListener {
binding.swipeRefreshLayout.isRefreshing = false
viewModel.getMain()
}
binding.ivCanFree.setOnClickListener {
PointClickAd.showOfferwall(requireActivity(), "무료충전")
curationViewModel.refresh()
bannerViewModel.getMainBannerList()
newContentViewModel.getThemeList()
newContentViewModel.getNewContentOfTheme("전체")
contentRankingViewModel.getContentRanking()
contentRankingViewModel.getContentRankingSortType()
newContentCreatorViewModel.getNewContentUploadCreatorList()
}
}
@ -144,6 +160,21 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
})
binding.rvNewContentCreator.adapter = newContentCreatorAdapter
newContentCreatorViewModel.newContentUploadCreatorListLiveData.observe(viewLifecycleOwner) {
newContentCreatorAdapter.addItems(it)
binding.rvNewContentCreator.visibility = if (
newContentCreatorAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
newContentCreatorViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupBanner() {
@ -208,6 +239,21 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
bannerViewModel.bannerLiveData.observe(viewLifecycleOwner) {
if (bannerAdapter.itemCount <= 0 && it.isEmpty()) {
binding.rvBanner.visibility = View.GONE
binding.indicatorBanner.visibility = View.GONE
} else {
binding.rvBanner.visibility = View.VISIBLE
binding.indicatorBanner.visibility = View.VISIBLE
binding.rvBanner.refreshData(it)
}
}
bannerViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupOrderList() {
@ -266,11 +312,26 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
binding.tvMyStashViewAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentOrderListActivity::class.java))
}
orderListViewModel.orderListLiveData.observe(viewLifecycleOwner) {
orderListAdapter.addItems(it)
binding.llMyStash.visibility = if (
orderListAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
orderListViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupNewContentTheme() {
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
viewModel.getNewContentOfTheme(theme = it)
newContentViewModel.getNewContentOfTheme(theme = it)
}
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
@ -308,6 +369,11 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
})
binding.rvNewContentTheme.adapter = newContentThemeAdapter
newContentViewModel.themeListLiveData.observe(viewLifecycleOwner) {
binding.llNewContent.visibility = View.VISIBLE
newContentThemeAdapter.addItems(it)
}
}
private fun setupNewContent() {
@ -367,11 +433,27 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
})
binding.rvNewContent.adapter = newContentAdapter
newContentViewModel.newContentListLiveData.observe(viewLifecycleOwner) {
newContentAdapter.addItems(it)
}
newContentViewModel.isLoading.observe(viewLifecycleOwner) {
binding.pbNewContent.visibility = if (it) {
View.VISIBLE
} else {
View.GONE
}
}
newContentViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupContentRankingSortType() {
contentRankingSortAdapter = AudioContentMainNewContentThemeAdapter {
viewModel.getContentRanking(sort = it)
contentRankingViewModel.getContentRanking(sort = it)
}
binding.rvContentRankingSort.layoutManager = LinearLayoutManager(
@ -409,8 +491,14 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
})
binding.rvContentRankingSort.adapter = contentRankingSortAdapter
contentRankingViewModel.contentRankingSortListLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
contentRankingSortAdapter.addItems(it)
}
}
@SuppressLint("SetTextI18n")
private fun setupContentRanking() {
binding.ivContentRankingAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentRankingAllActivity::class.java))
@ -449,6 +537,16 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
})
binding.rvContentRanking.adapter = contentRankingAdapter
contentRankingViewModel.contentRankingLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
binding.tvDate.text = "${it.startDate}~${it.endDate}"
contentRankingAdapter.addItems(it.items)
}
contentRankingViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupCuration() {
@ -511,85 +609,50 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
}
}
})
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
) {
curationViewModel.getCurationList()
}
}
})
binding.rvCuration.adapter = curationAdapter
}
@SuppressLint("SetTextI18n")
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
curationViewModel.curationListLiveData.observe(viewLifecycleOwner) {
if (curationViewModel.page == 2) {
curationAdapter.clear()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.newContentUploadCreatorListLiveData.observe(viewLifecycleOwner) {
newContentCreatorAdapter.addItems(it)
binding.rvNewContentCreator.visibility = if (
newContentCreatorAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
viewModel.bannerLiveData.observe(viewLifecycleOwner) {
if (bannerAdapter.itemCount <= 0 && it.isEmpty()) {
binding.rvBanner.visibility = View.GONE
binding.indicatorBanner.visibility = View.GONE
} else {
binding.rvBanner.visibility = View.VISIBLE
binding.indicatorBanner.visibility = View.VISIBLE
binding.rvBanner.refreshData(it)
}
}
viewModel.orderListLiveData.observe(viewLifecycleOwner) {
orderListAdapter.addItems(it)
binding.llMyStash.visibility = if (
orderListAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
viewModel.newContentListLiveData.observe(viewLifecycleOwner) {
newContentAdapter.addItems(it)
}
viewModel.themeListLiveData.observe(viewLifecycleOwner) {
binding.llNewContent.visibility = View.VISIBLE
newContentThemeAdapter.addItems(it)
}
viewModel.curationListLiveData.observe(viewLifecycleOwner) {
curationAdapter.addItems(it)
binding.rvCuration.visibility = if (
curationAdapter.itemCount <= 0 && it.isEmpty()
) {
binding.rvCuration.visibility = if (curationAdapter.itemCount <= 0 && it.isEmpty()) {
View.GONE
} else {
View.VISIBLE
}
}
viewModel.contentRankingSortListLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
contentRankingSortAdapter.addItems(it)
curationViewModel.isLoading.observe(viewLifecycleOwner) {
binding.pbCuration.visibility = if (it) {
View.VISIBLE
} else {
View.GONE
}
}
viewModel.contentRankingLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
binding.tvDate.text = "${it.startDate}~${it.endDate}"
contentRankingAdapter.addItems(it.items)
curationViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
}

View File

@ -1,166 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main
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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentMainViewModel(
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 _newContentUploadCreatorListLiveData =
MutableLiveData<List<GetNewContentUploadCreator>>()
val newContentUploadCreatorListLiveData: LiveData<List<GetNewContentUploadCreator>>
get() = _newContentUploadCreatorListLiveData
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _newContentListLiveData
private var _bannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
val bannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
get() = _bannerLiveData
private var _orderListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val orderListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _orderListLiveData
private var _themeListLiveData = MutableLiveData<List<String>>()
val themeListLiveData: LiveData<List<String>>
get() = _themeListLiveData
private var _curationListLiveData = MutableLiveData<List<GetAudioContentCurationResponse>>()
val curationListLiveData: LiveData<List<GetAudioContentCurationResponse>>
get() = _curationListLiveData
private var _contentRankingSortListLiveData = MutableLiveData<List<String>>()
val contentRankingSortListLiveData: LiveData<List<String>>
get() = _contentRankingSortListLiveData
private var _contentRankingLiveData = MutableLiveData<GetAudioContentRanking>()
val contentRankingLiveData: LiveData<GetAudioContentRanking>
get() = _contentRankingLiveData
fun getMain() {
_isLoading.value = true
compositeDisposable.add(
repository.getMain(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val data = it.data
_newContentUploadCreatorListLiveData.value =
data.newContentUploadCreatorList
_newContentListLiveData.value = data.newContentList
_orderListLiveData.value = data.orderList
_bannerLiveData.value = data.bannerList
_curationListLiveData.value = data.curationList
_contentRankingLiveData.value = data.contentRanking
_contentRankingSortListLiveData.value = data.contentRankingSortTypeList
val themeList = listOf("전체").union(data.themeList).toList()
_themeListLiveData.value = themeList
} 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 getNewContentOfTheme(theme: String) {
compositeDisposable.add(
repository.getNewContentOfTheme(
theme = if (theme == "전체") {
""
} else {
theme
},
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_newContentListLiveData.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 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

@ -3,18 +3,6 @@ package kr.co.vividnext.sodalive.audio_content.main
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.settings.event.EventItem
data class GetAudioContentMainResponse(
@SerializedName("newContentUploadCreatorList")
val newContentUploadCreatorList: List<GetNewContentUploadCreator>,
@SerializedName("bannerList") val bannerList: List<GetAudioContentBannerResponse>,
@SerializedName("orderList") val orderList: List<GetAudioContentMainItem>,
@SerializedName("themeList") val themeList: List<String>,
@SerializedName("newContentList") val newContentList: List<GetAudioContentMainItem>,
@SerializedName("curationList") val curationList: List<GetAudioContentCurationResponse>,
@SerializedName("contentRankingSortTypeList") val contentRankingSortTypeList: List<String>,
@SerializedName("contentRanking") val contentRanking: GetAudioContentRanking
)
data class GetNewContentUploadCreator(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,

View File

@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.audio_content.main
package kr.co.vividnext.sodalive.audio_content.main.banner
import android.content.Context
import android.graphics.Bitmap
@ -11,6 +11,7 @@ import com.bumptech.glide.request.transition.Transition
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.bannerview.BaseViewHolder
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
class AudioContentMainBannerAdapter(
private val context: Context,

View File

@ -0,0 +1,54 @@
package kr.co.vividnext.sodalive.audio_content.main.banner
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.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentMainBannerViewModel(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _bannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
val bannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
get() = _bannerLiveData
fun getMainBannerList() {
compositeDisposable.add(
repository.getMainBannerList(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_bannerLiveData.postValue(it.data!!)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"배너를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"배너를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
)
}
)
)
}
}

View File

@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.audio_content.main
package kr.co.vividnext.sodalive.audio_content.main.curation
import android.annotation.SuppressLint
import android.content.Context
@ -8,6 +8,9 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainContentAdapter
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentCurationResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainCurationBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
@ -89,8 +92,11 @@ class AudioContentMainCurationAdapter(
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetAudioContentCurationResponse>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
fun clear() {
this.items.clear()
}
}

View File

@ -0,0 +1,85 @@
package kr.co.vividnext.sodalive.audio_content.main.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.main.GetAudioContentCurationResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentMainCurationViewModel(
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 _curationListLiveData = MutableLiveData<List<GetAudioContentCurationResponse>>()
val curationListLiveData: LiveData<List<GetAudioContentCurationResponse>>
get() = _curationListLiveData
var page = 1
var isLast = false
private val pageSize = 10
fun getCurationList() {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getCurationList(
page = page,
size = pageSize,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
page += 1
if (it.data.isNotEmpty()) {
_curationListLiveData.postValue(it.data!!)
} else {
_curationListLiveData.postValue(listOf())
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"큐레이션을 불러오지 못했습니다. 다시 시도해 주세요.\n" +
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"큐레이션을 불러오지 못했습니다. 다시 시도해 주세요.\n" +
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
)
}
)
)
}
}
fun refresh() {
page = 1
isLast = false
getCurationList()
}
}

View File

@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.audio_content.main
package kr.co.vividnext.sodalive.audio_content.main.new_content
import android.annotation.SuppressLint
import android.content.Context
@ -28,9 +28,9 @@ class AudioContentMainNewContentThemeAdapter(
(selectedTheme == "" && theme == "매출")
) {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_9970ff
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
)
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_9970ff))
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_3bb9f1))
} else {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_777777

View File

@ -0,0 +1,96 @@
package kr.co.vividnext.sodalive.audio_content.main.new_content
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 AudioContentMainNewContentViewModel(
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 _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _newContentListLiveData
private var _themeListLiveData = MutableLiveData<List<String>>()
val themeListLiveData: LiveData<List<String>>
get() = _themeListLiveData
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 getNewContentOfTheme(theme: String) {
compositeDisposable.add(
repository.getNewContentOfTheme(
theme = if (theme == "전체") {
""
} else {
theme
},
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_newContentListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.audio_content.main
package kr.co.vividnext.sodalive.audio_content.main.new_content_upload_creator
import android.annotation.SuppressLint
import android.view.LayoutInflater
@ -7,6 +7,7 @@ import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.GetNewContentUploadCreator
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainNewContentCreatorBinding
class AudioContentMainNewContentCreatorAdapter(

View File

@ -0,0 +1,57 @@
package kr.co.vividnext.sodalive.audio_content.main.new_content_upload_creator
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.GetNewContentUploadCreator
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentMainNewContentCreatorViewModel(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _newContentUploadCreatorListLiveData =
MutableLiveData<List<GetNewContentUploadCreator>>()
val newContentUploadCreatorListLiveData: LiveData<List<GetNewContentUploadCreator>>
get() = _newContentUploadCreatorListLiveData
fun getNewContentUploadCreatorList() {
compositeDisposable.add(
repository.getNewContentUploadCreatorList(
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_newContentUploadCreatorListLiveData.postValue(it.data!!)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"크리에이터 리스트를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"크리에이터 리스트를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
)
}
)
)
}
}

View File

@ -0,0 +1,54 @@
package kr.co.vividnext.sodalive.audio_content.main.order
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 AudioContentMainOrderListViewModel(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _orderListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val orderListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _orderListLiveData
fun getOrderList() {
compositeDisposable.add(
repository.getMainOrderList(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_orderListLiveData.postValue(it.data!!)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"주문정보를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"주문정보를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
)
}
)
)
}
}

View File

@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.audio_content.main
package kr.co.vividnext.sodalive.audio_content.main.ranking
import android.annotation.SuppressLint
import android.view.LayoutInflater
@ -7,6 +7,7 @@ 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.ItemAudioContentMainRankingBinding
import kr.co.vividnext.sodalive.extensions.dpToPx

View File

@ -0,0 +1,86 @@
package kr.co.vividnext.sodalive.audio_content.main.ranking
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.GetAudioContentRanking
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentMainRankingViewModel(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
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 getContentRankingSortType() {
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 getContentRanking(sort: String = "매출") {
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) {
_contentRankingLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.audio_content.upload
import android.Manifest
import android.annotation.SuppressLint
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Intent
import android.net.Uri
import android.os.Build
@ -28,9 +30,15 @@ import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentUploadBinding
import kr.co.vividnext.sodalive.dialog.LiveDialog
import kr.co.vividnext.sodalive.dialog.SodaLiveTimePickerDialog
import kr.co.vividnext.sodalive.extensions.convertDateFormat
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBinding>(
ActivityAudioContentUploadBinding::inflate
@ -111,6 +119,26 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
}
}
private val datePickerDialogListener =
DatePickerDialog.OnDateSetListener { _, year, monthOfYear, dayOfMonth ->
viewModel.releaseDate = String.format("%d-%02d-%02d", year, monthOfYear + 1, dayOfMonth)
viewModel.setReservationDate(
String.format(
"%d.%02d.%02d",
year,
monthOfYear + 1,
dayOfMonth
)
)
}
private val timePickerDialogListener =
TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute ->
val timeString = String.format("%02d:%02d", hourOfDay, minute)
viewModel.releaseTime = timeString
viewModel.setReservationTime(timeString.convertDateFormat("HH:mm", "a hh:mm"))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkPermissions()
@ -173,6 +201,8 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
binding.llOnlyRental.setOnClickListener { viewModel.setIsOnlyRental(true) }
binding.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) }
binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) }
binding.llActiveNow.setOnClickListener { viewModel.setActiveReservation(false) }
binding.llActiveReservation.setOnClickListener { viewModel.setActiveReservation(true) }
binding.tvCancel.setOnClickListener { finish() }
binding.tvUpload.setOnClickListener {
@ -189,6 +219,71 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
).show(screenWidth)
}
}
binding.tvReservationDate.setOnClickListener {
val reservationDate = viewModel.releaseDate.split("-")
val datePicker: DatePickerDialog
if (reservationDate.isNotEmpty() && reservationDate.size == 3) {
datePicker = DatePickerDialog(
this,
R.style.DatePickerStyle,
datePickerDialogListener,
reservationDate[0].toInt(),
reservationDate[1].toInt() - 1,
reservationDate[2].toInt()
)
} else {
val dateString = SimpleDateFormat(
"yyyy.MM.dd",
Locale.getDefault()
).format(Date()).split(".")
datePicker = DatePickerDialog(
this,
R.style.DatePickerStyle,
datePickerDialogListener,
dateString[0].toInt(),
dateString[1].toInt() - 1,
dateString[2].toInt()
)
}
datePicker.show()
}
binding.tvReservationTime.setOnClickListener {
val reservationTime = viewModel.releaseTime.split(":")
val timePicker: TimePickerDialog
if (reservationTime.isNotEmpty() && reservationTime.size == 2) {
timePicker = SodaLiveTimePickerDialog(
this,
R.style.TimePickerStyle,
timePickerDialogListener,
reservationTime[0].toInt(),
reservationTime[1].toInt(),
false
)
} else {
val calendar = Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, 1)
}
val initialHour = calendar.get(Calendar.HOUR_OF_DAY)
val initialMinute = calendar.get(Calendar.MINUTE) / 15 * 15
timePicker = SodaLiveTimePickerDialog(
this,
R.style.TimePickerStyle,
timePickerDialogListener,
initialHour,
initialMinute,
false
)
}
timePicker.show()
}
}
private fun checkPermissions() {
@ -323,17 +418,17 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
R.color.color_eeeeee
)
)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentNo.visibility = View.GONE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llCommentNo.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
R.drawable.bg_round_corner_6_7_13181b
)
} else {
binding.ivCommentNo.visibility = View.VISIBLE
@ -343,17 +438,17 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
R.color.color_eeeeee
)
)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentYes.visibility = View.GONE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llCommentYes
.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734_9970ff)
.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
}
}
@ -369,16 +464,16 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
viewModel.isAdultLiveData.observe(this) {
if (it) {
binding.ivAgeAll.visibility = View.GONE
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.ivAge19.visibility = View.VISIBLE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
@ -387,16 +482,16 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
)
} else {
binding.ivAge19.visibility = View.GONE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.ivAgeAll.visibility = View.VISIBLE
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
@ -406,6 +501,69 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
}
}
}
viewModel.isActiveReservationLiveData.observe(this) {
if (it) {
checkActiveReservation()
} else {
checkActiveNow()
}
}
viewModel.reservationDateLiveData.observe(this) {
binding.tvReservationDate.text = it
}
viewModel.reservationTimeLiveData.observe(this) {
binding.tvReservationTime.text = it
}
}
private fun checkActiveNow() {
binding.llReservationDatetime.visibility = View.GONE
binding.ivActiveNow.visibility = View.VISIBLE
binding.tvActiveNow.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llActiveNow.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivActiveReservation.visibility = View.GONE
binding.tvActiveReservation.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.llActiveReservation.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
}
private fun checkActiveReservation() {
binding.llReservationDatetime.visibility = View.VISIBLE
binding.ivActiveReservation.visibility = View.VISIBLE
binding.tvActiveReservation.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llActiveReservation.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivActiveNow.visibility = View.GONE
binding.tvActiveNow.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.llActiveNow.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
}
private fun checkPriceFree() {
@ -422,17 +580,17 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
R.color.color_eeeeee
)
)
binding.llPriceFree.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llPriceFree.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivPricePaid.visibility = View.GONE
binding.tvPricePaid.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llPricePaid.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
R.drawable.bg_round_corner_6_7_13181b
)
binding.llConfigPreviewTime.visibility = View.GONE
@ -450,17 +608,17 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
R.color.color_eeeeee
)
)
binding.llPricePaid.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llPricePaid.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivPriceFree.visibility = View.GONE
binding.tvPriceFree.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llPriceFree.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
R.drawable.bg_round_corner_6_7_13181b
)
binding.llConfigPreviewTime.visibility = View.VISIBLE
}
@ -474,17 +632,17 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
R.color.color_eeeeee
)
)
binding.llRentalAndKeep.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llRentalAndKeep.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivOnlyRental.visibility = View.GONE
binding.tvOnlyRental.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llOnlyRental.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
R.drawable.bg_round_corner_6_7_13181b
)
}
@ -497,17 +655,17 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
R.color.color_eeeeee
)
)
binding.llOnlyRental.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llOnlyRental.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivRentalAndKeep.visibility = View.GONE
binding.tvRentalAndKeep.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llRentalAndKeep.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
R.drawable.bg_round_corner_6_7_13181b
)
}

View File

@ -20,6 +20,7 @@ import okio.BufferedSink
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class AudioContentUploadViewModel(
private val repository: AudioContentRepository
@ -49,12 +50,26 @@ class AudioContentUploadViewModel(
val isPriceFreeLiveData: LiveData<Boolean>
get() = _isPriceFreeLiveData
private val _isActiveReservationLiveData = MutableLiveData(false)
val isActiveReservationLiveData: LiveData<Boolean>
get() = _isActiveReservationLiveData
private val _reservationDateLiveData = MutableLiveData("날짜를 선택해주세요")
val reservationDateLiveData: LiveData<String>
get() = _reservationDateLiveData
private val _reservationTimeLiveData = MutableLiveData("시간을 설정해주세요")
val reservationTimeLiveData: LiveData<String>
get() = _reservationTimeLiveData
lateinit var getRealPathFromURI: (Uri) -> String?
var title = ""
var detail = ""
var tags = ""
var price = 0
var releaseDate = ""
var releaseTime = ""
var theme: GetAudioContentThemeResponse? = null
var coverImageUri: Uri? = null
var contentUri: Uri? = null
@ -81,6 +96,10 @@ class AudioContentUploadViewModel(
_isOnlyRentalLiveData.postValue(isOnlyRental)
}
fun setActiveReservation(isActiveReservation: Boolean) {
_isActiveReservationLiveData.postValue(isActiveReservation)
}
fun uploadAudioContent(onSuccess: () -> Unit) {
if (!_isLoading.value!! && validateData()) {
_isLoading.postValue(true)
@ -90,6 +109,12 @@ class AudioContentUploadViewModel(
detail = detail,
tags = tags,
price = price,
releaseDate = if (_isActiveReservationLiveData.value!!) {
"$releaseDate $releaseTime"
} else {
null
},
timezone = TimeZone.getDefault().id,
themeId = theme!!.id,
isAdult = _isAdultLiveData.value!!,
isOnlyRental = _isOnlyRentalLiveData.value!!,
@ -281,6 +306,14 @@ class AudioContentUploadViewModel(
return false
}
if (
_isActiveReservationLiveData.value!! &&
(releaseDate.isBlank() || releaseTime.isBlank())
) {
_toastLiveData.postValue("예약날짜와 시간을 선택해주세요.")
return false
}
return true
}
@ -307,4 +340,12 @@ class AudioContentUploadViewModel(
return 0
}
}
fun setReservationDate(dateString: String) {
_reservationDateLiveData.postValue(dateString)
}
fun setReservationTime(timeString: String) {
_reservationTimeLiveData.postValue(timeString)
}
}

View File

@ -7,10 +7,12 @@ data class CreateAudioContentRequest(
@SerializedName("detail") val detail: String,
@SerializedName("tags") val tags: String,
@SerializedName("price") val price: Int,
@SerializedName("releaseDate") val releaseDate: String?,
@SerializedName("timezone") val timezone: String,
@SerializedName("themeId") val themeId: Long,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isOnlyRental") val isOnlyRental: Boolean,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean,
@SerializedName("previewStartTime") val previewStartTime: String? = null,
@SerializedName("previewEndTime") val previewEndTime: String? = null
@SerializedName("previewEndTime") val previewEndTime: String? = null,
)

View File

@ -29,6 +29,7 @@ object Constants {
const val EXTRA_MESSAGE_BOX = "extra_message_box"
const val EXTRA_TEXT_MESSAGE = "extra_text_message"
const val EXTRA_LIVE_TIME_NOW = "extra_live_time_now"
const val EXTRA_RESULT_ROULETTE = "extra_result_roulette"
const val EXTRA_GO_TO_PREV_PAGE = "extra_go_to_prev_page"
const val EXTRA_SELECT_RECIPIENT = "extra_select_recipient"
const val EXTRA_ROOM_CHANNEL_NAME = "extra_room_channel_name"
@ -56,4 +57,8 @@ object Constants {
const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2
const val ACTION_AUDIO_CONTENT_RECEIVER = "soda_live_action_content_receiver"
const val ACTION_MAIN_AUDIO_CONTENT_RECEIVER = "soda_live_action_main_content_receiver"
const val EXTRA_COMMUNITY_POST_ID = "community_post_id"
const val EXTRA_COMMUNITY_CREATOR_ID = "community_creator_id"
const val EXTRA_COMMUNITY_POST_COMMENT = "community_post_comment_id"
}

View File

@ -0,0 +1,32 @@
package kr.co.vividnext.sodalive.common
import android.content.Context
import coil.ImageLoader
import okhttp3.Cache
import okhttp3.OkHttpClient
import java.io.File
object ImageLoaderProvider {
lateinit var imageLoader: ImageLoader
private set
val isInitialized: Boolean
get() = ::imageLoader.isInitialized
fun init(context: Context) {
val cacheSize = 250L * 1024L * 1024L // 250 MB
val cacheDirectory = File(
context.cacheDir,
"image_cache"
).apply { mkdirs() }
val cache = Cache(cacheDirectory, cacheSize)
imageLoader = ImageLoader.Builder(context)
.okHttpClient {
OkHttpClient().newBuilder()
.cache(cache)
.build()
}
.build()
}
}

View File

@ -14,7 +14,12 @@ import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentReplyVi
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentRepository
import kr.co.vividnext.sodalive.audio_content.curation.AudioContentCurationViewModel
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailViewModel
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainViewModel
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerViewModel
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationViewModel
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentViewModel
import kr.co.vividnext.sodalive.audio_content.main.new_content_upload_creator.AudioContentMainNewContentCreatorViewModel
import kr.co.vividnext.sodalive.audio_content.main.order.AudioContentMainOrderListViewModel
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingViewModel
import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyViewModel
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListViewModel
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadViewModel
@ -25,6 +30,12 @@ import kr.co.vividnext.sodalive.explorer.ExplorerApi
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.explorer.ExplorerViewModel
import kr.co.vividnext.sodalive.explorer.profile.UserProfileViewModel
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityApi
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllViewModel
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment.CreatorCommunityCommentListViewModel
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyViewModel
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteViewModel
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
@ -43,6 +54,9 @@ import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageViewMo
import kr.co.vividnext.sodalive.live.room.tag.LiveTagRepository
import kr.co.vividnext.sodalive.live.room.tag.LiveTagViewModel
import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditViewModel
import kr.co.vividnext.sodalive.live.roulette.RouletteRepository
import kr.co.vividnext.sodalive.live.roulette.config.RouletteApi
import kr.co.vividnext.sodalive.live.roulette.config.RouletteSettingsViewModel
import kr.co.vividnext.sodalive.main.MainViewModel
import kr.co.vividnext.sodalive.message.MessageApi
import kr.co.vividnext.sodalive.message.MessageRepository
@ -58,6 +72,7 @@ import kr.co.vividnext.sodalive.mypage.auth.AuthRepository
import kr.co.vividnext.sodalive.mypage.can.CanApi
import kr.co.vividnext.sodalive.mypage.can.CanRepository
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeViewModel
import kr.co.vividnext.sodalive.mypage.can.coupon.CanCouponViewModel
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentViewModel
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusViewModel
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateViewModel
@ -145,6 +160,8 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
single { ApiBuilder().build(get(), AudioContentApi::class.java) }
single { ApiBuilder().build(get(), FaqApi::class.java) }
single { ApiBuilder().build(get(), MemberTagApi::class.java) }
single { ApiBuilder().build(get(), RouletteApi::class.java) }
single { ApiBuilder().build(get(), CreatorCommunityApi::class.java) }
}
private val viewModelModule = module {
@ -153,7 +170,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { TermsViewModel(get()) }
viewModel { FindPasswordViewModel(get()) }
viewModel { MainViewModel(get(), get(), get(), get()) }
viewModel { LiveViewModel(get(), get(), get()) }
viewModel { LiveViewModel(get(), get(), get(), get()) }
viewModel { MyPageViewModel(get(), get()) }
viewModel { CanStatusViewModel(get()) }
viewModel { CanChargeViewModel(get()) }
@ -162,7 +179,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { LiveRoomCreateViewModel(get()) }
viewModel { LiveTagViewModel(get()) }
viewModel { LiveRoomEditViewModel(get()) }
viewModel { LiveRoomViewModel(get(), get(), get()) }
viewModel { LiveRoomViewModel(get(), get(), get(), get()) }
viewModel { LiveRoomDonationMessageViewModel(get()) }
viewModel { ExplorerViewModel(get()) }
viewModel { UserProfileViewModel(get(), get(), get()) }
@ -179,7 +196,12 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { SettingsViewModel(get()) }
viewModel { TextMessageDetailViewModel(get()) }
viewModel { LiveReservationStatusViewModel(get()) }
viewModel { AudioContentMainViewModel(get()) }
viewModel { AudioContentMainBannerViewModel(get()) }
viewModel { AudioContentMainRankingViewModel(get()) }
viewModel { AudioContentMainCurationViewModel(get()) }
viewModel { AudioContentMainOrderListViewModel(get()) }
viewModel { AudioContentMainNewContentViewModel(get()) }
viewModel { AudioContentMainNewContentCreatorViewModel(get()) }
viewModel { AudioContentViewModel(get()) }
viewModel { AudioContentOrderListViewModel(get()) }
viewModel { AudioContentUploadViewModel(get()) }
@ -197,6 +219,12 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { AudioContentCurationViewModel(get()) }
viewModel { AudioContentNewAllViewModel(get()) }
viewModel { AudioContentRankingAllViewModel(get()) }
viewModel { RouletteSettingsViewModel(get()) }
viewModel { CreatorCommunityAllViewModel(get(), get()) }
viewModel { CreatorCommunityCommentListViewModel(get()) }
viewModel { CreatorCommunityWriteViewModel(get()) }
viewModel { CreatorCommunityModifyViewModel(get()) }
viewModel { CanCouponViewModel(get()) }
}
private val repositoryModule = module {
@ -219,6 +247,8 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
factory { FaqRepository(get()) }
factory { MemberTagRepository(get()) }
factory { UserProfileFantalkAllViewModel(get(), get()) }
factory { RouletteRepository(get()) }
factory { CreatorCommunityRepository(get()) }
}
private val moduleList = listOf(

View File

@ -0,0 +1,45 @@
package kr.co.vividnext.sodalive.dialog
import android.app.TimePickerDialog
import android.content.Context
import android.widget.TimePicker
class SodaLiveTimePickerDialog(
context: Context,
themeResId: Int,
private val onTimeSetListener: OnTimeSetListener,
hourOfDay: Int,
minute: Int,
is24HourView: Boolean
) : TimePickerDialog(context, themeResId, null, hourOfDay, minute, is24HourView) {
private var timePicker: TimePicker? = null
init {
this.setTitle("Select Time")
setOnShowListener {
timePicker = window?.findViewById(
context.resources.getIdentifier(
"android:id/timePicker",
null,
null
)
)
timePicker?.apply {
setIs24HourView(is24HourView)
setOnTimeChangedListener { _, _, minute ->
// Snap minute to nearest quarter (0, 15, 30, 45)
val snappedMinute = minute / 15 * 15
if (snappedMinute != minute) {
this.minute = snappedMinute
}
}
}
getButton(BUTTON_POSITIVE).setOnClickListener {
timePicker?.let { picker ->
onTimeSetListener.onTimeSet(picker, picker.hour, picker.minute)
}
dismiss()
}
}
}
}

View File

@ -1,73 +0,0 @@
package kr.co.vividnext.sodalive.explorer.profile
import android.content.Intent
import android.widget.Toast
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityCreatorNoticeWriteBinding
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import org.koin.android.ext.android.inject
class CreatorNoticeWriteActivity : BaseActivity<ActivityCreatorNoticeWriteBinding>(
ActivityCreatorNoticeWriteBinding::inflate
) {
private val repository: ExplorerRepository by inject()
private lateinit var loadingDialog: LoadingDialog
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "공지사항 쓰기"
binding.toolbar.tvBack.setOnClickListener { finish() }
val notice = intent.getStringExtra("notice")
binding.etContent.setText(notice)
binding.tvSave.setOnClickListener {
loadingDialog.show(screenWidth)
val writtenNotice = binding.etContent.text.toString()
compositeDisposable.add(
repository.writeCreatorNotice(
notice = writtenNotice,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
loadingDialog.dismiss()
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
Toast.makeText(
applicationContext,
message,
Toast.LENGTH_LONG
).show()
if (it.success) {
val dataIntent = Intent()
dataIntent.putExtra("notice", writtenNotice)
setResult(RESULT_OK, dataIntent)
finish()
}
},
{
loadingDialog.dismiss()
it.message?.let { message -> Logger.e(message) }
Toast.makeText(
applicationContext,
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.",
Toast.LENGTH_LONG
).show()
}
)
)
}
}
}

View File

@ -1,20 +1,21 @@
package kr.co.vividnext.sodalive.explorer.profile
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
data class GetCreatorProfileResponse(
@SerializedName("creator")
val creator: CreatorResponse,
@SerializedName("userDonationRanking")
val userDonationRanking: List<UserDonationRankingResponse>,
@SerializedName("similarCreatorList")
val similarCreatorList: List<SimilarCreatorResponse>,
@SerializedName("liveRoomList")
val liveRoomList: List<LiveRoomResponse>,
@SerializedName("contentList")
val contentList: List<GetAudioContentListItem>,
@SerializedName("notice")
val notice: String,
@SerializedName("communityPostList")
val communityPostList: List<GetCommunityPostListResponse>,
@SerializedName("cheers")
val cheers: GetCheersResponse,
@SerializedName("activitySummary")
@ -45,13 +46,6 @@ data class UserDonationRankingResponse(
@SerializedName("donationCan") val donationCan: Int
)
data class SimilarCreatorResponse(
@SerializedName("userId") val userId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImage") val profileImage: String,
@SerializedName("tags") val tags: List<String>
)
data class LiveRoomResponse(
@SerializedName("roomId") val roomId: Long,
@SerializedName("title") val title: String,
@ -82,7 +76,8 @@ data class GetAudioContentListItem(
@SerializedName("duration") val duration: String?,
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentCount") val commentCount: Int,
@SerializedName("isAdult") val isAdult: Boolean
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isScheduledToOpen") val isScheduledToOpen: Boolean
)
data class GetCreatorActivitySummary(

View File

@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.explorer.profile
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.app.Service
import android.content.Context
@ -11,17 +10,18 @@ import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.webkit.URLUtil
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.LinearLayoutManager
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.AudioContentActivity
import kr.co.vividnext.sodalive.audio_content.AudioContentAdapter
@ -33,12 +33,17 @@ 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.ActivityUserProfileBinding
import kr.co.vividnext.sodalive.databinding.ItemCreatorCommunityBinding
import kr.co.vividnext.sodalive.explorer.profile.cheers.UserProfileCheersAdapter
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAdapter
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAllViewActivity
import kr.co.vividnext.sodalive.explorer.profile.fantalk.UserProfileFantalkAllViewActivity
import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.reservation.complete.LiveReservationCompleteActivity
@ -50,6 +55,9 @@ import kr.co.vividnext.sodalive.report.ProfileReportDialog
import kr.co.vividnext.sodalive.report.ReportType
import kr.co.vividnext.sodalive.report.UserReportDialog
import org.koin.android.ext.android.inject
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
ActivityUserProfileBinding::inflate
@ -63,11 +71,8 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
private lateinit var liveAdapter: UserProfileLiveAdapter
private lateinit var audioContentAdapter: AudioContentAdapter
private lateinit var donationAdapter: UserProfileDonationAdapter
private lateinit var similarCreatorAdapter: UserProfileSimilarCreatorAdapter
private lateinit var cheersAdapter: UserProfileCheersAdapter
private lateinit var noticeWriteLauncher: ActivityResultLauncher<Intent>
private val handler = Handler(Looper.getMainLooper())
private var userId: Long = 0
@ -76,17 +81,6 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
super.onCreate(savedInstanceState)
imm = getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager
noticeWriteLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
val writtenNotice = it.data?.getStringExtra("notice")
binding.tvNotice.text = writtenNotice?.ifBlank {
"공지사항이 없습니다."
}
}
}
if (userId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
@ -122,9 +116,9 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
setupLiveView()
setupDonationView()
setupSimilarCreatorView()
setupFanTalkView()
setupAudioContentListView()
setupCreatorCommunityView()
}
private fun hideKeyboard(onAfterExecute: () -> Unit) {
@ -310,51 +304,6 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
recyclerView.adapter = donationAdapter
}
private fun setupSimilarCreatorView() {
val recyclerView = binding.layoutUserProfileSimilarCreator.rvSimilarCreator
similarCreatorAdapter = UserProfileSimilarCreatorAdapter {
val intent = Intent(applicationContext, UserProfileActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, it.userId)
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)) {
0 -> {
outRect.top = 0
outRect.bottom = 10f.dpToPx().toInt()
}
similarCreatorAdapter.itemCount - 1 -> {
outRect.top = 10f.dpToPx().toInt()
outRect.bottom = 0
}
else -> {
outRect.top = 10f.dpToPx().toInt()
outRect.bottom = 10f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = similarCreatorAdapter
}
private fun setupFanTalkView() {
binding.layoutUserProfileFanTalk.tvAll.setOnClickListener {
val intent = Intent(
@ -528,6 +477,19 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
dialog.show(screenWidth)
}
private fun setupCreatorCommunityView() {
binding.layoutCreatorCommunityPost.ivWrite.setOnClickListener {
startActivity(Intent(applicationContext, CreatorCommunityWriteActivity::class.java))
}
binding.layoutCreatorCommunityPost.llAll.setOnClickListener {
startActivity(
Intent(applicationContext, CreatorCommunityAllActivity::class.java).apply {
putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, userId)
}
)
}
}
private fun bindData() {
liveViewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
@ -556,20 +518,11 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
viewModel.creatorProfileLiveData.observe(this) {
setCheers(it.cheers)
setCreatorProfile(it.creator)
setCreatorNotice(it.notice, it.creator.creatorId)
setAudioContentList(it.contentList)
setLiveRoomList(it.liveRoomList)
setSimilarCreatorList(it.similarCreatorList)
setUserDonationRanking(it.userDonationRanking)
setActivitySummary(it.activitySummary)
}
viewModel.isExpandNotice.observe(this) {
if (it) {
binding.tvNotice.maxLines = Int.MAX_VALUE
} else {
binding.tvNotice.maxLines = 1
}
setCommunityPostList(it.communityPostList)
}
}
@ -684,28 +637,6 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
binding.layoutUserProfileIntroduce.tvIntroduce.text = introduce
}
private fun setCreatorNotice(notice: String, creatorId: Long) {
binding.tvNotice.text = notice.ifBlank {
"공지사항이 없습니다."
}
binding.rlNotice.setOnClickListener {
if (creatorId == SharedPreferenceManager.userId) {
val intent = Intent(applicationContext, CreatorNoticeWriteActivity::class.java)
intent.putExtra("notice", notice)
noticeWriteLauncher.launch(intent)
} else {
viewModel.toggleExpandNotice()
}
}
binding.ivWrite.visibility = if (creatorId == SharedPreferenceManager.userId) {
View.VISIBLE
} else {
View.GONE
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setAudioContentList(audioContentList: List<GetAudioContentListItem>) {
binding.layoutUserProfileAudioContent.root.visibility =
@ -748,18 +679,6 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setSimilarCreatorList(similarCreatorList: List<SimilarCreatorResponse>) {
if (similarCreatorList.isEmpty()) {
binding.llUserProfileSimilarCreator.visibility = View.GONE
} else {
binding.llUserProfileSimilarCreator.visibility = View.VISIBLE
similarCreatorAdapter.items.clear()
similarCreatorAdapter.items.addAll(similarCreatorList)
similarCreatorAdapter.notifyDataSetChanged()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setUserDonationRanking(userDonationRanking: List<UserDonationRankingResponse>) {
if (userDonationRanking.isEmpty()) {
@ -772,6 +691,88 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
}
}
private fun setCommunityPostList(communityPostList: List<GetCommunityPostListResponse>) {
if (communityPostList.isEmpty()) {
if (userId == SharedPreferenceManager.userId) {
binding.layoutCreatorCommunityPost.root.visibility = View.VISIBLE
binding.layoutCreatorCommunityPost.llNoPost.visibility = View.VISIBLE
binding.layoutCreatorCommunityPost.llNoPost.setOnClickListener {
startActivity(
Intent(
applicationContext,
CreatorCommunityWriteActivity::class.java
)
)
}
} else {
binding.layoutCreatorCommunityPost.root.visibility = View.GONE
}
binding.layoutCreatorCommunityPost.hsvPost.visibility = View.GONE
} else {
binding.layoutCreatorCommunityPost.root.visibility = View.VISIBLE
binding.layoutCreatorCommunityPost.llNoPost.visibility = View.GONE
binding.layoutCreatorCommunityPost.hsvPost.visibility = View.VISIBLE
if (userId == SharedPreferenceManager.userId) {
binding.layoutCreatorCommunityPost.ivWrite.visibility = View.VISIBLE
} else {
binding.layoutCreatorCommunityPost.ivWrite.visibility = View.GONE
}
binding.layoutCreatorCommunityPost.llContainer.removeAllViews()
communityPostList.forEachIndexed { index, item ->
val layout = ItemCreatorCommunityBinding.inflate(
LayoutInflater.from(this@UserProfileActivity),
binding.layoutCreatorCommunityPost.llContainer,
false
)
setCommunityPost(layout, item, index)
}
}
}
private fun setCommunityPost(
layout: ItemCreatorCommunityBinding,
item: GetCommunityPostListResponse,
index: Int
) {
layout.ivCreatorProfile.loadUrl(item.creatorProfileUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
layout.tvCreatorNickname.text = item.creatorNickname
layout.tvDate.text = item.date
layout.tvContent.text = item.content
layout.ivPostImage.loadUrl(item.imageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
}
layout.tvLikeCount.text = "${item.likeCount}"
layout.tvCommentCount.text = "${item.commentCount}"
layout.root.setOnClickListener {
startActivity(
Intent(applicationContext, CreatorCommunityAllActivity::class.java).apply {
putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, userId)
}
)
}
if (index > 0) {
val lp = layout.root.layoutParams as LinearLayout.LayoutParams
lp.marginStart = 13.3f.dpToPx().toInt()
layout.root.layoutParams = lp
}
binding.layoutCreatorCommunityPost.llContainer.addView(layout.root)
}
private fun reservationRoom(roomId: Long) {
liveViewModel.getRoomDetail(roomId) {
if (it.manager.id == SharedPreferenceManager.userId) {
@ -850,6 +851,15 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
}
} else {
val beginDateFormat = SimpleDateFormat("yyyy.MM.dd EEE hh:mm a", Locale.ENGLISH)
val beginDate = beginDateFormat.parse(it.beginDateTime)!!
val now = Date()
val dateFormat = SimpleDateFormat("yyyy-MM-dd, HH:mm", Locale.getDefault())
val diffTime: Long = now.time - beginDate.time
val hours = (diffTime / (1000 * 60 * 60)).toInt()
val mins = (diffTime / (1000 * 60)).toInt() % 60
if (it.isPrivateRoom) {
LiveRoomPasswordDialog(
activity = this,
@ -867,8 +877,23 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
LivePaymentDialog(
activity = this,
layoutInflater = layoutInflater,
title = "${it.price.moneyFormat()}캔으로 입장",
desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.",
title = "유료 라이브 입장",
startDateTime = if (hours >= 1) {
dateFormat.format(beginDate)
} else {
null
},
nowDateTime = if (hours >= 1) {
dateFormat.format(now)
} else {
null
},
desc = "${it.price}캔을 차감하고\n라이브에 입장 하시겠습니까?",
desc2 = if (hours >= 1) {
"라이브를 시작한 지 ${hours}시간 ${mins}분이 지났습니다. 라이브에 입장 후 30분 이내에 라이브가 종료될 수도 있습니다."
} else {
null
},
confirmButtonTitle = "결제 후 입장",
confirmButtonClick = {
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)

View File

@ -1,46 +0,0 @@
package kr.co.vividnext.sodalive.explorer.profile
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemUserProfileSimilarCreatorBinding
class UserProfileSimilarCreatorAdapter(
private val onClickItem: (SimilarCreatorResponse) -> Unit
) : RecyclerView.Adapter<UserProfileSimilarCreatorAdapter.ViewHolder>() {
val items = mutableListOf<SimilarCreatorResponse>()
inner class ViewHolder(
private val binding: ItemUserProfileSimilarCreatorBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: SimilarCreatorResponse) {
binding.ivProfile.load(item.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvNickname.text = item.nickname
binding.tvTags.text = item.tags.joinToString(" ") { "#$it" }
binding.root.setOnClickListener { onClickItem(item) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemUserProfileSimilarCreatorBinding.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

@ -16,6 +16,7 @@ import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.report.ReportRequest
import kr.co.vividnext.sodalive.report.ReportType
@ -38,10 +39,6 @@ class UserProfileViewModel(
val creatorProfileLiveData: LiveData<GetCreatorProfileResponse>
get() = _creatorProfileLiveData
private val _isExpandNotice = MutableLiveData(false)
val isExpandNotice: LiveData<Boolean>
get() = _isExpandNotice
private var creatorNickname = ""
fun cheersReport(cheersId: Long, reason: String) {
@ -216,10 +213,6 @@ class UserProfileViewModel(
)
}
fun toggleExpandNotice() {
_isExpandNotice.value = !isExpandNotice.value!!
}
fun writeCheers(parentCheersId: Long? = null, creatorId: Long, cheersContent: String) {
if (cheersContent.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")

View File

@ -0,0 +1,58 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemCreatorCommunityBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl
class CreatorCommunityAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<CreatorCommunityAdapter.ViewHolder>() {
val items = mutableListOf<GetCommunityPostListResponse>()
inner class ViewHolder(
private val binding: ItemCreatorCommunityBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetCommunityPostListResponse) {
binding.ivCreatorProfile.loadUrl(item.creatorProfileUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvCreatorNickname.text = item.creatorNickname
binding.tvDate.text = item.date
binding.tvContent.text = item.content
binding.ivPostImage.loadUrl(item.imageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
}
binding.tvLikeCount.text = "${item.likeCount}"
binding.tvCommentCount.text = "${item.commentCount}"
binding.root.setOnClickListener { onClickItem(item.creatorId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemCreatorCommunityBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
}

View File

@ -0,0 +1,94 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.PostCommunityPostLikeRequest
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment.CreateCommunityPostCommentRequest
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
interface CreatorCommunityApi {
@POST("/creator-community")
@Multipart
fun createCommunityPost(
@Part postImage: MultipartBody.Part?,
@Part("request") request: RequestBody,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@PUT("/creator-community")
@Multipart
fun modifyCommunityPost(
@Part postImage: MultipartBody.Part?,
@Part("request") request: RequestBody,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/creator-community")
fun getCommunityPostList(
@Query("creatorId") creatorId: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetCommunityPostListResponse>>>
@GET("/creator-community/latest")
fun getLatestPostListFromCreatorsYouFollow(
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetCommunityPostListResponse>>>
@POST("/creator-community/like")
fun communityPostLike(
@Body request: PostCommunityPostLikeRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/creator-community/{id}/comment")
fun getCommunityPostCommentList(
@Path("id") postId: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetCommunityPostCommentListResponse>>
@POST("/creator-community/comment")
fun createCommunityPostComment(
@Body request: CreateCommunityPostCommentRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@PUT("/creator-community/comment")
fun modifyComment(
@Body request: ModifyCommentRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/creator-community/comment/{id}")
fun getCommentReplyList(
@Path("id") commentId: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetCommunityPostCommentListResponse>>
@GET("/creator-community/{id}")
fun getCommunityPostDetail(
@Path("id") postId: Long,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetCommunityPostListResponse>>
}

View File

@ -0,0 +1,101 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community
import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.PostCommunityPostLikeRequest
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment.CreateCommunityPostCommentRequest
import okhttp3.MultipartBody
import okhttp3.RequestBody
import java.util.TimeZone
class CreatorCommunityRepository(private val api: CreatorCommunityApi) {
fun getCommunityPostList(
creatorId: Long,
page: Int,
size: Int,
token: String
) = api.getCommunityPostList(
creatorId = creatorId,
page = page - 1,
size = size,
timezone = TimeZone.getDefault().id,
authHeader = token
)
fun getLatestPostListFromCreatorsYouFollow(
token: String
) = api.getLatestPostListFromCreatorsYouFollow(
timezone = TimeZone.getDefault().id,
authHeader = token
)
fun communityPostLike(postId: Long, token: String) = api.communityPostLike(
request = PostCommunityPostLikeRequest(postId = postId),
authHeader = token
)
fun getCommunityPostCommentList(
postId: Long,
page: Int,
size: Int,
token: String
) = api.getCommunityPostCommentList(
postId = postId,
page = page,
size = size,
timezone = TimeZone.getDefault().id,
authHeader = token
)
fun registerComment(
postId: Long,
comment: String,
parentId: Long? = null,
token: String
) = api.createCommunityPostComment(
request = CreateCommunityPostCommentRequest(
comment = comment,
postId = postId,
parentId = parentId
),
authHeader = token
)
fun modifyComment(request: ModifyCommentRequest, token: String) = api.modifyComment(
request = request,
authHeader = token
)
fun getCommentReplyList(commentId: Long, page: Int, size: Int, token: String) = api.getCommentReplyList(
commentId = commentId,
page = page,
size = size,
timezone = TimeZone.getDefault().id,
authHeader = token
)
fun createCommunityPost(
postImage: MultipartBody.Part?,
request: RequestBody,
token: String
) = api.createCommunityPost(
postImage = postImage,
request = request,
authHeader = token
)
fun modifyCommunityPost(
postImage: MultipartBody.Part?,
request: RequestBody,
token: String
) = api.modifyCommunityPost(
postImage = postImage,
request = request,
authHeader = token
)
fun getCommunityPostDetail(postId: Long, token: String) = api.getCommunityPostDetail(
postId = postId,
timezone = TimeZone.getDefault().id,
authHeader = token
)
}

View File

@ -0,0 +1,21 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
data class GetCommunityPostCommentListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetCommunityPostCommentListItem>
)
@Parcelize
data class GetCommunityPostCommentListItem(
@SerializedName("id") val id: Long,
@SerializedName("writerId") val writerId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("comment") val comment: String,
@SerializedName("date") val date: String,
@SerializedName("replyCount") val replyCount: Int,
) : Parcelable

View File

@ -0,0 +1,20 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community
import com.google.gson.annotations.SerializedName
data class GetCommunityPostListResponse(
@SerializedName("postId") val postId: Long,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("creatorProfileUrl") val creatorProfileUrl: String,
@SerializedName("imageUrl") val imageUrl: String?,
@SerializedName("content") val content: String,
@SerializedName("date") val date: String,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isLike") var isLike: Boolean,
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentCount") val commentCount: Int,
@SerializedName("firstComment") val firstComment: GetCommunityPostCommentListItem?,
@SerializedName("isExpand") var isExpand: Boolean = false
)

View File

@ -0,0 +1,195 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.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.activity.result.contract.ActivityResultContracts
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityCreatorCommunityAllBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment.CreatorCommunityCommentFragment
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBinding>(
ActivityCreatorCommunityAllBinding::inflate
) {
private val viewModel: CreatorCommunityAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: CreatorCommunityAllAdapter
private var creatorId: Long = 0
private val modifyResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
if (resultCode == RESULT_OK) {
viewModel.page = 1
viewModel.isLast = false
viewModel.getCommunityPostList()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
creatorId = intent.getLongExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, 0)
if (creatorId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
bindData()
viewModel.creatorId = creatorId
viewModel.getCommunityPostList()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "커뮤니티"
binding.toolbar.tvBack.setOnClickListener { finish() }
adapter = CreatorCommunityAllAdapter(
onClickLike = { viewModel.communityPostLike(it) },
writeComment = { postId, parentId, comment ->
viewModel.registerComment(
comment,
postId,
parentId
)
},
showCommentBottomSheetDialog = {
val dialog = CreatorCommunityCommentFragment(
creatorId = creatorId,
postId = it
)
dialog.show(
supportFragmentManager,
dialog.tag
)
},
onClickModify = {
modifyResult.launch(
Intent(
applicationContext,
CreatorCommunityModifyActivity::class.java
).apply {
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, it)
}
)
},
onClickDelete = { postId ->
SodaDialog(
activity = this@CreatorCommunityAllActivity,
layoutInflater = layoutInflater,
title = "게시물 삭제",
desc = "삭제하시겠습니까?",
confirmButtonTitle = "삭제",
confirmButtonClick = {
viewModel.deleteCommunityPostList(postId = postId)
},
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
},
onClickReport = { postId ->
CreatorCommunityReportDialog(this@CreatorCommunityAllActivity, layoutInflater) {
viewModel.report(
communityPostId = postId,
reason = it
)
}.show(screenWidth)
}
)
val recyclerView = binding.rvCreatorCommunity
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 = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
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()
}
}
}
})
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.getCommunityPostList()
}
}
})
recyclerView.adapter = adapter
}
@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.communityPostListLiveData.observe(this) {
if (viewModel.page == 2) {
adapter.items.clear()
}
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
}
}

View File

@ -0,0 +1,215 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.all
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.text.Spannable
import android.text.SpannableString
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ItemCreatorCommunityAllBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
import kr.co.vividnext.sodalive.extensions.loadUrl
import java.util.regex.Pattern
class CreatorCommunityAllAdapter(
private val onClickLike: (Long) -> Unit,
private val writeComment: (Long, Long?, String) -> Unit,
private val showCommentBottomSheetDialog: (Long) -> Unit,
private val onClickModify: (Long) -> Unit,
private val onClickDelete: (Long) -> Unit,
private val onClickReport: (Long) -> Unit
) : RecyclerView.Adapter<CreatorCommunityAllAdapter.ViewHolder>() {
val items = mutableListOf<GetCommunityPostListResponse>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemCreatorCommunityAllBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(item: GetCommunityPostListResponse, index: Int) {
binding.tvDate.text = item.date
binding.tvNickname.text = item.creatorNickname
binding.ivCreatorProfile.loadUrl(item.creatorProfileUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
setNoticeAndClickableUrl(binding.tvContent, item.content)
binding.tvContent.setOnClickListener {
items[index] = items[index].copy(
isExpand = !item.isExpand,
)
notifyDataSetChanged()
}
binding.tvContent.maxLines = if (item.isExpand) {
Int.MAX_VALUE
} else {
3
}
binding.ivSeeMore.setOnClickListener {
showOptionMenu(
context = context,
v = binding.ivSeeMore,
postId = item.postId,
creatorId = item.creatorId
)
}
binding.ivLike.setImageResource(
if (item.isLike) {
R.drawable.ic_audio_content_heart_pressed
} else {
R.drawable.ic_audio_content_heart_normal
}
)
binding.tvLike.text = "${item.likeCount}"
binding.llLike.setOnClickListener {
val isLike = !item.isLike
items[index] = items[index].copy(
isLike = !item.isLike,
likeCount = if (isLike) item.likeCount + 1 else item.likeCount - 1
)
notifyDataSetChanged()
onClickLike(item.postId)
}
if (item.imageUrl != null) {
binding.ivContent.visibility = View.VISIBLE
binding.ivContent.loadUrl(item.imageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
}
} else {
binding.ivContent.visibility = View.GONE
}
if (item.isCommentAvailable) {
binding.llComment.visibility = View.VISIBLE
binding.tvCommentCount.text = "${item.commentCount}"
} else {
binding.llComment.visibility = View.GONE
}
if (item.commentCount > 0) {
binding.ivCommentProfile.load(item.firstComment!!.profileUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
binding.tvCommentText.text = item.firstComment.comment
binding.tvCommentText.visibility = View.VISIBLE
binding.rlInputComment.visibility = View.GONE
binding.llComment.setOnClickListener { showCommentBottomSheetDialog(item.postId) }
} else {
binding.tvCommentText.visibility = View.GONE
binding.rlInputComment.visibility = View.VISIBLE
binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
binding.ivCommentSend.setOnClickListener {
val comment = binding.etComment.text.toString()
binding.etComment.setText("")
writeComment(item.postId, null, comment)
}
binding.llComment.setOnClickListener {}
}
}
private fun setNoticeAndClickableUrl(textView: TextView, text: String) {
textView.text = text
val spannable = SpannableString(text)
val pattern = Pattern.compile("https?://\\S+")
val matcher = pattern.matcher(spannable)
while (matcher.find()) {
val start = matcher.start()
val end = matcher.end()
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
val url = spannable.subSequence(start, end).toString()
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
}
spannable.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
textView.text = spannable
textView.movementMethod = LinkMovementMethod.getInstance()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemCreatorCommunityAllBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], position)
}
private fun showOptionMenu(
context: Context,
v: View,
postId: Long,
creatorId: Long
) {
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
if (creatorId == SharedPreferenceManager.userId) {
inflater.inflate(R.menu.community_post_creator_option_menu, popup.menu)
} else {
inflater.inflate(R.menu.community_post_option_menu, popup.menu)
}
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_modify -> {
onClickModify(postId)
}
R.id.menu_delete -> {
onClickDelete(postId)
}
R.id.menu_report -> {
onClickReport(postId)
}
}
true
}
popup.show()
}
}

View File

@ -0,0 +1,223 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.all
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.Gson
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.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.ModifyCommunityPostRequest
import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.report.ReportRequest
import kr.co.vividnext.sodalive.report.ReportType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
class CreatorCommunityAllViewModel(
private val repository: CreatorCommunityRepository,
private val reportRepository: ReportRepository
) : 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 _communityPostListLiveData = MutableLiveData<List<GetCommunityPostListResponse>>()
val communityPostListLiveData: LiveData<List<GetCommunityPostListResponse>>
get() = _communityPostListLiveData
var page = 1
var isLast = false
private val pageSize = 10
var creatorId = 0L
fun getCommunityPostList() {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository
.getCommunityPostList(
creatorId = creatorId,
page = page,
size = pageSize,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.isNotEmpty()) {
page += 1
_communityPostListLiveData.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 deleteCommunityPostList(postId: Long) {
if (!_isLoading.value!!) {
_isLoading.value = true
val request = ModifyCommunityPostRequest(
creatorCommunityId = postId,
isActive = false
)
val requestJson = Gson().toJson(request)
compositeDisposable.add(
repository.modifyCommunityPost(
null,
request = requestJson.toRequestBody("text/plain".toMediaType()),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
page = 1
isLast = false
getCommunityPostList()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
)
)
}
}
fun communityPostLike(postId: Long) {
compositeDisposable.add(
repository.communityPostLike(
postId = postId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({}, {})
)
}
fun registerComment(comment: String, postId: Long, parentId: Long? = null) {
if (!_isLoading.value!!) {
_isLoading.value = true
}
compositeDisposable.add(
repository.registerComment(
postId = postId,
comment = comment,
parentId = parentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
page = 1
isLast = false
getCommunityPostList()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun report(communityPostId: Long, reason: String) {
_isLoading.value = true
val request = ReportRequest(
type = ReportType.COMMUNITY_POST,
reason = reason,
communityPostId = communityPostId
)
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("신고가 접수되었습니다.")
}
)
)
}
}

View File

@ -0,0 +1,60 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.all
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.WindowManager
import android.widget.RadioButton
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import kr.co.vividnext.sodalive.databinding.DialogCommunityPostReportBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class CreatorCommunityReportDialog(
activity: Activity,
layoutInflater: LayoutInflater,
confirmButtonClick: (String) -> Unit
) {
private val alertDialog: AlertDialog
val dialogView = DialogCommunityPostReportBinding.inflate(layoutInflater)
var reason = ""
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.tvReport.setOnClickListener {
if (reason.isNotBlank()) {
alertDialog.dismiss()
confirmButtonClick(reason)
} else {
Toast.makeText(activity, "신고 이유를 선택하세요.", Toast.LENGTH_LONG).show()
}
}
dialogView.radioGroup.setOnCheckedChangeListener { radioGroup, checkedId ->
val radioButton = radioGroup.findViewById<RadioButton>(checkedId)
reason = radioButton.text.toString()
}
}
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

@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.all
import com.google.gson.annotations.SerializedName
data class PostCommunityPostLikeRequest(
@SerializedName("postId") val postId: Long
)

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment
import com.google.gson.annotations.SerializedName
data class CreateCommunityPostCommentRequest(
@SerializedName("comment") val comment: String,
@SerializedName("postId") val postId: Long,
@SerializedName("parentId") val parentId: Long?
)

View File

@ -0,0 +1,128 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ItemCommunityPostCommentBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostCommentListItem
class CreatorCommunityCommentAdapter(
private val creatorId: Long,
private val modifyComment: (Long, String) -> Unit,
private val onClickDelete: (Long) -> Unit,
private val onItemClick: (GetCommunityPostCommentListItem) -> Unit
) : RecyclerView.Adapter<CreatorCommunityCommentAdapter.ViewHolder>() {
var items = mutableSetOf<GetCommunityPostCommentListItem>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemCommunityPostCommentBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetCommunityPostCommentListItem) {
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
binding.tvComment.text = item.comment
binding.tvCommentDate.text = item.date
binding.tvCommentNickname.text = item.nickname
binding.tvWriteReply.text = if (item.replyCount > 0) {
"답글 ${item.replyCount}"
} else {
"답글 쓰기"
}
if (
item.writerId == SharedPreferenceManager.userId ||
creatorId == SharedPreferenceManager.userId
) {
binding.etCommentModify.setText(item.comment)
binding.ivMenu.visibility = View.VISIBLE
binding.ivMenu.setOnClickListener {
showOptionMenu(
context,
binding.ivMenu,
commentId = item.id,
writerId = item.writerId,
creatorId = creatorId,
onClickModify = {
binding.rlCommentModify.visibility = View.VISIBLE
binding.tvComment.visibility = View.GONE
}
)
}
binding.tvModify.setOnClickListener {
binding.rlCommentModify.visibility = View.GONE
binding.tvComment.visibility = View.VISIBLE
modifyComment(item.id, binding.etCommentModify.text.toString())
}
} else {
binding.ivMenu.visibility = View.GONE
}
binding.tvWriteReply.setOnClickListener { onItemClick(item) }
binding.root.setOnClickListener { onItemClick(item) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemCommunityPostCommentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items.toList()[position])
}
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

@ -0,0 +1,78 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.DialogAudioContentCommentBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostCommentListItem
class CreatorCommunityCommentFragment(
private val creatorId: Long,
private val postId: Long
) : BottomSheetDialogFragment() {
private lateinit var binding: DialogAudioContentCommentBinding
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.setOnShowListener {
val d = it as BottomSheetDialog
val bottomSheet = d.findViewById<FrameLayout>(
com.google.android.material.R.id.design_bottom_sheet
)
if (bottomSheet != null) {
BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_EXPANDED
}
}
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DialogAudioContentCommentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val commentListFragmentTag = "COMMENT_LIST_FRAGMENT"
val commentListFragment = CreatorCommunityCommentListFragment.newInstance(
creatorId = creatorId,
postId = postId
)
val fragmentTransaction = childFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.fl_container, commentListFragment, commentListFragmentTag)
fragmentTransaction.addToBackStack(commentListFragmentTag)
fragmentTransaction.commit()
}
fun hideCommentDialog() {
dialog?.dismiss()
}
fun onClickComment(comment: GetCommunityPostCommentListItem) {
val commentReplyFragmentTag = "COMMENT_REPLY_FRAGMENT"
val commentReplyFragment = CreatorCommunityCommentReplyFragment.newInstance(
creatorId = creatorId,
postId = postId,
comment = comment
)
val fragmentTransaction = childFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.fl_container, commentReplyFragment, commentReplyFragmentTag)
fragmentTransaction.addToBackStack(commentReplyFragmentTag)
fragmentTransaction.commit()
}
}

View File

@ -0,0 +1,214 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment
import android.annotation.SuppressLint
import android.app.Service
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentCommentListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class CreatorCommunityCommentListFragment : BaseFragment<FragmentAudioContentCommentListBinding>(
FragmentAudioContentCommentListBinding::inflate
) {
private val viewModel: CreatorCommunityCommentListViewModel by inject()
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: CreatorCommunityCommentAdapter
private var creatorId: Long = 0
private var postId: Long = 0
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
creatorId = arguments?.getLong(Constants.EXTRA_COMMUNITY_CREATOR_ID) ?: 0
postId = arguments?.getLong(Constants.EXTRA_COMMUNITY_POST_ID) ?: 0
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireContext().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
setupView()
bindData()
viewModel.getCommentList(postId) { hideDialog() }
}
private fun hideDialog() {
(parentFragment as CreatorCommunityCommentFragment).hideCommentDialog()
}
private fun setupView() {
binding.ivClose.setOnClickListener { hideDialog() }
binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.ivCommentSend.setOnClickListener {
hideKeyboard()
val comment = binding.etComment.text.toString()
binding.etComment.setText("")
viewModel.registerComment(postId, comment)
}
adapter = CreatorCommunityCommentAdapter(
creatorId = creatorId,
modifyComment = { commentId, comment ->
hideKeyboard()
viewModel.modifyComment(
commentId = commentId,
postId = postId,
comment = comment
)
},
onClickDelete = {
SodaDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
title = "댓글 삭제",
desc = "삭제하시겠습니까?",
confirmButtonTitle = "삭제",
confirmButtonClick = {
viewModel.modifyComment(
commentId = it,
postId = postId,
isActive = false
)
},
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
},
onItemClick = {
(parentFragment as CreatorCommunityCommentFragment).onClickComment(it)
}
)
val recyclerView = binding.rvComment
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(
activity,
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 = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
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()
}
}
}
})
recyclerView.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.getCommentList(postId = postId)
}
}
})
recyclerView.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.totalCommentCount.observe(viewLifecycleOwner) {
binding.tvCommentCount.text = "$it"
}
viewModel.commentList.observe(viewLifecycleOwner) {
if (viewModel.page - 1 == 1) {
adapter.items.clear()
binding.rvComment.scrollToPosition(0)
}
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
}
private fun hideKeyboard() {
imm.hideSoftInputFromWindow(view?.windowToken, 0)
}
companion object {
fun newInstance(creatorId: Long, postId: Long): CreatorCommunityCommentListFragment {
val args = Bundle()
args.putLong(Constants.EXTRA_COMMUNITY_CREATOR_ID, creatorId)
args.putLong(Constants.EXTRA_COMMUNITY_POST_ID, postId)
val fragment = CreatorCommunityCommentListFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -0,0 +1,248 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment
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.comment.ModifyCommentRequest
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostCommentListItem
class CreatorCommunityCommentListViewModel(
private val repository: CreatorCommunityRepository
) : 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 _commentList = MutableLiveData<List<GetCommunityPostCommentListItem>>()
val commentList: LiveData<List<GetCommunityPostCommentListItem>>
get() = _commentList
private var _totalCommentCount = MutableLiveData(0)
val totalCommentCount: LiveData<Int>
get() = _totalCommentCount
var page = 1
private var isLast = false
private val size = 10
fun getCommentList(postId: Long, onFailure: (() -> Unit)? = null) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getCommunityPostCommentList(
postId = postId,
page = page - 1,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_totalCommentCount.postValue(it.data.totalCount)
page += 1
if (it.data.items.isNotEmpty()) {
_commentList.postValue(it.data.items)
} else {
isLast = true
_commentList.postValue(listOf())
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (onFailure != null) {
onFailure()
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
)
)
}
}
fun registerComment(postId: Long, comment: String, commentId: Long? = null) {
if (!_isLoading.value!!) {
_isLoading.value = true
}
compositeDisposable.add(
repository.registerComment(
postId = postId,
comment = comment,
parentId = commentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
page = 1
isLast = false
if (commentId != null) {
getCommentReplyList(commentId = commentId)
} else {
getCommentList(postId)
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun modifyComment(
commentId: Long,
postId: Long,
parentCommentId: Long? = null,
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
if (parentCommentId != null) {
getCommentReplyList(parentCommentId)
} else {
getCommentList(postId)
}
} else {
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun getCommentReplyList(commentId: Long, onFailure: (() -> Unit)? = null) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getCommentReplyList(
commentId = commentId,
page = page - 1,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
page += 1
if (it.data.items.isNotEmpty()) {
_commentList.postValue(it.data.items)
} else {
isLast = true
_commentList.postValue(listOf())
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (onFailure != null) {
onFailure()
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
)
)
}
}
}

View File

@ -0,0 +1,173 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ItemCommunityPostCommentBinding
import kr.co.vividnext.sodalive.databinding.ItemCommunityPostCommentReplyBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostCommentListItem
import kr.co.vividnext.sodalive.extensions.loadUrl
class CreatorCommunityCommentReplyAdapter(
private val creatorId: Long,
private val modifyComment: (Long, String) -> Unit,
private val onClickDelete: (Long) -> Unit
) : RecyclerView.Adapter<CreatorCommunityCommentReplyViewHolder>() {
var items = mutableSetOf<GetCommunityPostCommentListItem>()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): CreatorCommunityCommentReplyViewHolder {
return if (viewType == 0) {
CreatorCommunityCommentReplyHeaderViewHolder(
binding = ItemCommunityPostCommentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
} else {
CreatorCommunityCommentReplyItemViewHolder(
context = parent.context,
creatorId = creatorId,
binding = ItemCommunityPostCommentReplyBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
showOptionMenu = { context, view, commentId, writerId, creatorId, onClickModify ->
showOptionMenu(context, view, commentId, writerId, creatorId, onClickModify)
},
modifyComment = modifyComment
)
}
}
override fun onBindViewHolder(holder: CreatorCommunityCommentReplyViewHolder, position: Int) {
holder.bind(items.toList()[position])
}
override fun getItemCount() = items.size
override fun getItemViewType(position: Int): Int {
return position
}
private fun showOptionMenu(
context: Context,
v: View,
commentId: Long,
writerId: Long,
creatorId: Long,
onClickModify: () -> Unit
) {
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
if (writerId == SharedPreferenceManager.userId) {
inflater.inflate(R.menu.content_comment_option_menu, popup.menu)
} else if (creatorId == SharedPreferenceManager.userId) {
inflater.inflate(R.menu.content_comment_option_menu2, popup.menu)
}
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_review_modify -> {
onClickModify()
}
R.id.menu_review_delete -> {
onClickDelete(commentId)
}
}
true
}
popup.show()
}
}
abstract class CreatorCommunityCommentReplyViewHolder(
binding: ViewBinding
) : RecyclerView.ViewHolder(binding.root) {
abstract fun bind(item: GetCommunityPostCommentListItem)
}
class CreatorCommunityCommentReplyHeaderViewHolder(
private val binding: ItemCommunityPostCommentBinding
) : CreatorCommunityCommentReplyViewHolder(binding) {
override fun bind(item: GetCommunityPostCommentListItem) {
binding.ivCommentProfile.loadUrl(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvComment.text = item.comment
binding.tvCommentDate.text = item.date
binding.tvCommentNickname.text = item.nickname
binding.tvWriteReply.visibility = View.GONE
binding.ivMenu.visibility = View.GONE
}
}
class CreatorCommunityCommentReplyItemViewHolder(
private val context: Context,
private val creatorId: Long,
private val binding: ItemCommunityPostCommentReplyBinding,
private val showOptionMenu: (
Context, View, Long, Long, Long, onClickModify: () -> Unit
) -> Unit,
private val modifyComment: (Long, String) -> Unit
) : CreatorCommunityCommentReplyViewHolder(binding) {
override fun bind(item: GetCommunityPostCommentListItem) {
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvComment.text = item.comment
binding.tvCommentDate.text = item.date
binding.tvCommentNickname.text = item.nickname
if (
item.writerId == SharedPreferenceManager.userId ||
creatorId == SharedPreferenceManager.userId
) {
binding.etCommentModify.setText(item.comment)
binding.ivMenu.visibility = View.VISIBLE
binding.ivMenu.setOnClickListener {
showOptionMenu(
context,
binding.ivMenu,
item.id,
item.writerId,
creatorId
) {
binding.rlCommentModify.visibility = View.VISIBLE
binding.tvComment.visibility = View.GONE
}
}
binding.tvModify.setOnClickListener {
binding.rlCommentModify.visibility = View.GONE
binding.tvComment.visibility = View.VISIBLE
modifyComment(item.id, binding.etCommentModify.text.toString())
}
} else {
binding.ivMenu.visibility = View.GONE
}
}
}

View File

@ -0,0 +1,239 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment
import android.annotation.SuppressLint
import android.app.Service
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.core.os.BundleCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentCommentReplyBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostCommentListItem
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class CreatorCommunityCommentReplyFragment : BaseFragment<FragmentAudioContentCommentReplyBinding>(
FragmentAudioContentCommentReplyBinding::inflate
) {
private val viewModel: CreatorCommunityCommentListViewModel by inject()
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: CreatorCommunityCommentReplyAdapter
private var originalComment: GetCommunityPostCommentListItem? = null
private var creatorId: Long = 0
private var postId: Long = 0
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
creatorId = arguments?.getLong(Constants.EXTRA_COMMUNITY_CREATOR_ID) ?: 0
postId = arguments?.getLong(Constants.EXTRA_COMMUNITY_POST_ID) ?: 0
originalComment = BundleCompat.getParcelable(
requireArguments(),
Constants.EXTRA_COMMUNITY_POST_COMMENT,
GetCommunityPostCommentListItem::class.java
)
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (originalComment == null) {
parentFragmentManager.popBackStack()
}
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireContext().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
setupView()
bindData()
viewModel.getCommentReplyList(commentId = originalComment!!.id) {
parentFragmentManager.popBackStack()
}
}
private fun hideDialog() {
(parentFragment as CreatorCommunityCommentFragment).hideCommentDialog()
}
private fun setupView() {
binding.root.setOnClickListener { }
binding.tvBack.setOnClickListener {
parentFragmentManager.popBackStack()
}
binding.ivClose.setOnClickListener { hideDialog() }
binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.ivCommentSend.setOnClickListener {
hideKeyboard()
val comment = binding.etComment.text.toString()
binding.etComment.setText("")
viewModel.registerComment(postId, comment, originalComment!!.id)
}
adapter = CreatorCommunityCommentReplyAdapter(
creatorId = creatorId,
modifyComment = { commentId, comment ->
hideKeyboard()
viewModel.modifyComment(
commentId = commentId,
postId = postId,
parentCommentId = originalComment!!.id,
comment = comment
)
},
onClickDelete = {
SodaDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
title = "댓글 삭제",
desc = "삭제하시겠습니까?",
confirmButtonTitle = "삭제",
confirmButtonClick = {
viewModel.modifyComment(
commentId = it,
postId = postId,
parentCommentId = originalComment!!.id,
isActive = false
)
},
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
},
).apply {
items.add(originalComment!!)
}
val recyclerView = binding.rvCommentReply
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(
activity,
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 = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 12f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 12f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
else -> {
outRect.top = 12f.dpToPx().toInt()
outRect.bottom = 12f.dpToPx().toInt()
}
}
}
})
recyclerView.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.getCommentReplyList(originalComment!!.id)
}
}
})
recyclerView.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.commentList.observe(viewLifecycleOwner) {
if (viewModel.page - 1 == 1) {
adapter.items.clear()
binding.rvCommentReply.scrollToPosition(0)
adapter.items.add(originalComment!!)
}
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
}
private fun hideKeyboard() {
imm.hideSoftInputFromWindow(view?.windowToken, 0)
}
companion object {
fun newInstance(
creatorId: Long,
postId: Long,
comment: GetCommunityPostCommentListItem
): CreatorCommunityCommentReplyFragment {
val args = Bundle()
args.putLong(Constants.EXTRA_COMMUNITY_POST_ID, postId)
args.putLong(Constants.EXTRA_COMMUNITY_CREATOR_ID, creatorId)
args.putParcelable(Constants.EXTRA_COMMUNITY_POST_COMMENT, comment)
val fragment = CreatorCommunityCommentReplyFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -0,0 +1,282 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.modify
import android.Manifest
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import coil.load
import coil.transform.RoundedCornersTransformation
import com.github.dhaval2404.imagepicker.ImagePicker
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityCreatorCommunityModifyBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl
import org.koin.android.ext.android.inject
class CreatorCommunityModifyActivity : BaseActivity<ActivityCreatorCommunityModifyBinding>(
ActivityCreatorCommunityModifyBinding::inflate
) {
private val viewModel: CreatorCommunityModifyViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private val imageResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = data?.data
if (fileUri != null) {
binding.ivContent.background = null
binding.ivContent.load(fileUri) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(8f.dpToPx()))
}
viewModel.imageUri = fileUri
} else {
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkPermissions()
viewModel.getRealPathFromURI = {
RealPathUtil.getRealPath(applicationContext, it)
}
bindData()
val postId = intent.getLongExtra(Constants.EXTRA_COMMUNITY_POST_ID, 0)
if (postId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
viewModel.postId = postId
viewModel.getCommunityPostDetail(
onFailure = {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
)
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "게시글 등록"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.ivPhotoPicker.setOnClickListener {
ImagePicker.with(this)
.crop()
.galleryOnly()
.galleryMimeTypes( // Exclude gif images
mimeTypes = arrayOf(
"image/png",
"image/jpg",
"image/jpeg"
)
)
.createIntent { imageResult.launch(it) }
}
if (SharedPreferenceManager.isAuth) {
binding.llSetAdult.visibility = View.VISIBLE
} else {
binding.llSetAdult.visibility = View.GONE
}
binding.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) }
binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) }
binding.tvCancel.setOnClickListener { finish() }
binding.tvUpload.setOnClickListener {
viewModel.modifyCommunityPost {
setResult(RESULT_OK)
finish()
}
}
}
private fun checkPermissions() {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
listOf(Manifest.permission.READ_MEDIA_AUDIO, Manifest.permission.READ_MEDIA_IMAGES)
} else {
listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}
TedPermission.create()
.setPermissionListener(object : PermissionListener {
override fun onPermissionGranted() {
}
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
finish()
}
})
.setDeniedMessage(R.string.read_storage_permission_denied_message)
.setPermissions(*permissions.toTypedArray())
.check()
}
@SuppressLint("SetTextI18n")
private fun bindData() {
compositeDisposable.add(
binding.etContent.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
binding.tvNumberOfCharacters.text = "${it.length}"
viewModel.content = it.toString()
}
)
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.imageUrlLiveData.observe(this) {
if (!it.isNullOrBlank()) {
binding.ivContent.background = null
binding.ivContent.loadUrl(it) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(8f.dpToPx()))
}
}
}
viewModel.contentLiveData.observe(this) {
binding.etContent.setText(it)
}
viewModel.isAvailableCommentLiveData.observe(this) {
if (it) {
binding.ivCommentYes.visibility = View.VISIBLE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.white
)
)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentNo.visibility = View.GONE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_80d8ff
)
)
binding.llCommentNo.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
} else {
binding.ivCommentNo.visibility = View.VISIBLE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.white
)
)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentYes.visibility = View.GONE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_80d8ff
)
)
binding.llCommentYes
.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
}
}
if (SharedPreferenceManager.isAuth) {
binding.llAgeAll.setOnClickListener {
viewModel.setAdult(false)
}
binding.llAge19.setOnClickListener {
viewModel.setAdult(true)
}
viewModel.isAdultLiveData.observe(this) {
if (it) {
binding.ivAgeAll.visibility = View.GONE
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_80d8ff
)
)
binding.ivAge19.visibility = View.VISIBLE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.white
)
)
} else {
binding.ivAge19.visibility = View.GONE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_80d8ff
)
)
binding.ivAgeAll.visibility = View.VISIBLE
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.white
)
)
}
}
}
}
}

View File

@ -0,0 +1,209 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.modify
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.Gson
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.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink
import java.io.File
class CreatorCommunityModifyViewModel(
private val repository: CreatorCommunityRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _imageUrlLiveData = MutableLiveData<String?>()
val imageUrlLiveData: LiveData<String?>
get() = _imageUrlLiveData
private val _contentLiveData = MutableLiveData("")
val contentLiveData: LiveData<String>
get() = _contentLiveData
private val _isAdultLiveData = MutableLiveData(false)
val isAdultLiveData: LiveData<Boolean>
get() = _isAdultLiveData
private val _isAvailableCommentLiveData = MutableLiveData(true)
val isAvailableCommentLiveData: LiveData<Boolean>
get() = _isAvailableCommentLiveData
lateinit var getRealPathFromURI: (Uri) -> String?
var postId = 0L
var content = ""
var imageUri: Uri? = null
private var communityPost: GetCommunityPostListResponse? = null
fun setAdult(isAdult: Boolean) {
_isAdultLiveData.postValue(isAdult)
}
fun setAvailableComment(isAvailableComment: Boolean) {
_isAvailableCommentLiveData.postValue(isAvailableComment)
}
fun getCommunityPostDetail(onFailure: (() -> Unit)? = null) {
communityPost = null
_isLoading.value = true
compositeDisposable.add(
repository.getCommunityPostDetail(
postId = postId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
communityPost = it.data
_imageUrlLiveData.value = it.data.imageUrl
_contentLiveData.value = it.data.content
_isAdultLiveData.value = it.data.isAdult
_isAvailableCommentLiveData.value = it.data.isCommentAvailable
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (onFailure != null) {
onFailure()
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
)
)
}
fun modifyCommunityPost(onSuccess: () -> Unit) {
if (!_isLoading.value!! && validateData()) {
_isLoading.value = true
val request = ModifyCommunityPostRequest(
creatorCommunityId = postId,
content = if (communityPost!!.content != content) {
content
} else {
null
},
isCommentAvailable = if (
communityPost!!.isCommentAvailable != _isAvailableCommentLiveData.value!!
) {
_isAvailableCommentLiveData.value!!
} else {
null
},
isAdult = if (communityPost!!.isAdult != _isAdultLiveData.value!!) {
_isAdultLiveData.value!!
} else {
null
}
)
val requestJson = Gson().toJson(request)
val postImage = if (imageUri != null) {
val file = File(getRealPathFromURI(imageUri!!))
MultipartBody.Part.createFormData(
"postImage",
file.name,
body = object : RequestBody() {
override fun contentType(): MediaType {
return "image/*".toMediaType()
}
override fun writeTo(sink: BufferedSink) {
file.inputStream().use { inputStream ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
sink.write(buffer, 0, bytesRead)
}
}
}
override fun contentLength(): Long {
return file.length()
}
}
)
} else {
null
}
compositeDisposable.add(
repository.modifyCommunityPost(
postImage,
request = requestJson.toRequestBody("text/plain".toMediaType()),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
)
)
}
}
private fun validateData(): Boolean {
if (content.isBlank() || content.length < 5) {
_toastLiveData.postValue("내용을 5자 이상 입력해 주세요.")
return false
}
return true
}
}

View File

@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.modify
import com.google.gson.annotations.SerializedName
data class ModifyCommunityPostRequest(
@SerializedName("creatorCommunityId") val creatorCommunityId: Long,
@SerializedName("content") val content: String? = null,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean? = null,
@SerializedName("isAdult") val isAdult: Boolean? = null,
@SerializedName("isActive") val isActive: Boolean? = null
)

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.write
import com.google.gson.annotations.SerializedName
data class CreateCommunityPostRequest(
@SerializedName("content") val content: String,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean
)

View File

@ -0,0 +1,248 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.write
import android.Manifest
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import coil.load
import coil.transform.RoundedCornersTransformation
import com.github.dhaval2404.imagepicker.ImagePicker
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityCreatorCommunityWriteBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWriteBinding>(
ActivityCreatorCommunityWriteBinding::inflate
) {
private val viewModel: CreatorCommunityWriteViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private val imageResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = data?.data
if (fileUri != null) {
binding.ivContent.background = null
binding.ivContent.load(fileUri) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(8f.dpToPx()))
}
viewModel.imageUri = fileUri
} else {
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkPermissions()
viewModel.getRealPathFromURI = {
RealPathUtil.getRealPath(applicationContext, it)
}
bindData()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "게시글 등록"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.ivPhotoPicker.setOnClickListener {
ImagePicker.with(this)
.crop()
.galleryOnly()
.galleryMimeTypes( // Exclude gif images
mimeTypes = arrayOf(
"image/png",
"image/jpg",
"image/jpeg"
)
)
.createIntent { imageResult.launch(it) }
}
if (SharedPreferenceManager.isAuth) {
binding.llSetAdult.visibility = View.VISIBLE
} else {
binding.llSetAdult.visibility = View.GONE
}
binding.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) }
binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) }
binding.tvCancel.setOnClickListener { finish() }
binding.tvUpload.setOnClickListener {
viewModel.createCommunityPost { finish() }
}
}
private fun checkPermissions() {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
listOf(Manifest.permission.READ_MEDIA_AUDIO, Manifest.permission.READ_MEDIA_IMAGES)
} else {
listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}
TedPermission.create()
.setPermissionListener(object : PermissionListener {
override fun onPermissionGranted() {
}
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
finish()
}
})
.setDeniedMessage(R.string.read_storage_permission_denied_message)
.setPermissions(*permissions.toTypedArray())
.check()
}
@SuppressLint("SetTextI18n")
private fun bindData() {
compositeDisposable.add(
binding.etContent.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
binding.tvNumberOfCharacters.text = "${it.length}"
viewModel.content = it.toString()
}
)
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.isAvailableCommentLiveData.observe(this) {
if (it) {
binding.ivCommentYes.visibility = View.VISIBLE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.white
)
)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentNo.visibility = View.GONE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_80d8ff
)
)
binding.llCommentNo.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
} else {
binding.ivCommentNo.visibility = View.VISIBLE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.white
)
)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentYes.visibility = View.GONE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_80d8ff
)
)
binding.llCommentYes
.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
}
}
if (SharedPreferenceManager.isAuth) {
binding.llAgeAll.setOnClickListener {
viewModel.setAdult(false)
}
binding.llAge19.setOnClickListener {
viewModel.setAdult(true)
}
viewModel.isAdultLiveData.observe(this) {
if (it) {
binding.ivAgeAll.visibility = View.GONE
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_80d8ff
)
)
binding.ivAge19.visibility = View.VISIBLE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.white
)
)
} else {
binding.ivAge19.visibility = View.GONE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_80d8ff
)
)
binding.ivAgeAll.visibility = View.VISIBLE
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.white
)
)
}
}
}
}
}

View File

@ -0,0 +1,136 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.write
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.Gson
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.profile.creator_community.CreatorCommunityRepository
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink
import java.io.File
class CreatorCommunityWriteViewModel(private val repository: CreatorCommunityRepository
): BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _isAdultLiveData = MutableLiveData(false)
val isAdultLiveData: LiveData<Boolean>
get() = _isAdultLiveData
private val _isAvailableCommentLiveData = MutableLiveData(true)
val isAvailableCommentLiveData: LiveData<Boolean>
get() = _isAvailableCommentLiveData
lateinit var getRealPathFromURI: (Uri) -> String?
var content = ""
var imageUri: Uri? = null
fun setAdult(isAdult: Boolean) {
_isAdultLiveData.postValue(isAdult)
}
fun setAvailableComment(isAvailableComment: Boolean) {
_isAvailableCommentLiveData.postValue(isAvailableComment)
}
fun createCommunityPost(onSuccess: () -> Unit) {
if (!_isLoading.value!! && validateData()) {
_isLoading.postValue(true)
val request = CreateCommunityPostRequest(
content = content,
isAdult = _isAdultLiveData.value!!,
isCommentAvailable = _isAvailableCommentLiveData.value!!
)
val requestJson = Gson().toJson(request)
val postImage = if (imageUri != null) {
val file = File(getRealPathFromURI(imageUri!!))
MultipartBody.Part.createFormData(
"postImage",
file.name,
body = object : RequestBody() {
override fun contentType(): MediaType {
return "image/*".toMediaType()
}
override fun writeTo(sink: BufferedSink) {
file.inputStream().use { inputStream ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
sink.write(buffer, 0, bytesRead)
}
}
}
override fun contentLength(): Long {
return file.length()
}
}
)
} else {
null
}
compositeDisposable.add(
repository.createCommunityPost(
postImage = postImage,
request = requestJson.toRequestBody("text/plain".toMediaType()),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
onSuccess()
} 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(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
)
)
}
}
private fun validateData(): Boolean {
if (content.isBlank() || content.length < 5) {
_toastLiveData.postValue("내용을 5자 이상 입력해 주세요.")
return false
}
return true
}
}

View File

@ -0,0 +1,14 @@
package kr.co.vividnext.sodalive.extensions
import android.widget.ImageView
import coil.load
import coil.request.ImageRequest
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
import kr.co.vividnext.sodalive.common.ImageLoaderProvider.imageLoader
fun ImageView.loadUrl(url: String?, builder: ImageRequest.Builder.() -> Unit = {}) {
if (!ImageLoaderProvider.isInitialized) {
throw IllegalStateException("ImageLoaderProvider is not initialized")
}
this.load(url, imageLoader, builder)
}

View File

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

View File

@ -27,6 +27,8 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentLiveBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityAdapter
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.following.FollowingCreatorActivity
@ -49,6 +51,9 @@ import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditActivity
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.notification.MemberRole
import org.koin.android.ext.android.inject
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.roundToInt
class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::inflate) {
@ -56,6 +61,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
private lateinit var liveNowAdapter: LiveNowAdapter
private lateinit var liveReservationAdapter: LiveReservationAdapter
private lateinit var creatorCommunityAdapter: CreatorCommunityAdapter
private lateinit var liveRecommendChannelAdapter: LiveRecommendChannelAdapter
private lateinit var loadingDialog: LoadingDialog
@ -91,6 +97,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
setupLiveNow()
setupLiveReservation()
setupEvent()
setupCommunityPost()
message = "라이브를 불러오고 있습니다."
viewModel.getSummary()
@ -494,6 +501,68 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setupCommunityPost() {
val recyclerView = binding.rvCommunityPost
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
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)) {
0 -> {
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 5.dpToPx().toInt()
}
liveNowAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
creatorCommunityAdapter = CreatorCommunityAdapter {
startActivity(
Intent(
requireActivity(),
CreatorCommunityAllActivity::class.java
).apply {
putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, it)
}
)
}
binding.rvCommunityPost.adapter = creatorCommunityAdapter
viewModel.communityPostItemLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.rvCommunityPost.visibility = View.VISIBLE
creatorCommunityAdapter.items.clear()
creatorCommunityAdapter.items.addAll(it)
creatorCommunityAdapter.notifyDataSetChanged()
} else {
binding.rvCommunityPost.visibility = View.GONE
}
}
}
private fun startLive(roomId: Long) {
val onEnterRoomSuccess = {
viewModel.getSummary()
@ -629,6 +698,15 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
}, 300)
}
} else {
val beginDateFormat = SimpleDateFormat("yyyy.MM.dd EEE hh:mm a", Locale.ENGLISH)
val beginDate = beginDateFormat.parse(it.beginDateTime)!!
val now = Date()
val dateFormat = SimpleDateFormat("yyyy-MM-dd, HH:mm", Locale.getDefault())
val diffTime: Long = now.time - beginDate.time
val hours = (diffTime / (1000 * 60 * 60)).toInt()
val mins = (diffTime / (1000 * 60)).toInt() % 60
if (it.isPrivateRoom) {
LiveRoomPasswordDialog(
activity = requireActivity(),
@ -648,8 +726,23 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
LivePaymentDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
title = "${it.price.moneyFormat()}캔으로 입장",
desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.",
title = "유료 라이브 입장",
startDateTime = if (hours >= 1) {
dateFormat.format(beginDate)
} else {
null
},
nowDateTime = if (hours >= 1) {
dateFormat.format(now)
} else {
null
},
desc = "${it.price}캔을 차감하고\n라이브에 입장 하시겠습니까?",
desc2 = if (hours >= 1) {
"라이브를 시작한 지 ${hours}시간 ${mins}분이 지났습니다. 라이브에 입장 후 30분 이내에 라이브가 종료될 수도 있습니다."
} else {
null
},
confirmButtonTitle = "결제 후 입장",
confirmButtonClick = {
handler.postDelayed({

View File

@ -8,6 +8,8 @@ import io.reactivex.rxjava3.core.Flowable
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.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
import kr.co.vividnext.sodalive.live.recommend.GetRecommendLiveResponse
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepository
import kr.co.vividnext.sodalive.live.recommend_channel.GetRecommendChannelResponse
@ -24,7 +26,8 @@ import kr.co.vividnext.sodalive.settings.event.EventRepository
class LiveViewModel(
private val repository: LiveRepository,
private val eventRepository: EventRepository,
private val liveRecommendRepository: LiveRecommendRepository
private val liveRecommendRepository: LiveRecommendRepository,
private val creatorCommunityRepository: CreatorCommunityRepository
) : BaseViewModel() {
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
@ -60,6 +63,10 @@ class LiveViewModel(
val eventLiveData: LiveData<List<EventItem>>
get() = _eventLiveData
private val _communityPostItemLiveData = MutableLiveData<List<GetCommunityPostListResponse>>()
val communityPostItemLiveData: LiveData<List<GetCommunityPostListResponse>>
get() = _communityPostItemLiveData
var page = 1
var isLast = false
private val pageSize = 10
@ -135,6 +142,36 @@ class LiveViewModel(
)
}
private fun getLatestPostListFromCreatorsYouFollow() {
compositeDisposable.add(
creatorCommunityRepository.getLatestPostListFromCreatorsYouFollow(
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_communityPostItemLiveData.postValue(it.data!!)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun getSummary() {
if (!_isLoading.value!!) {
if (_isFollowedCreatorLive.value!!) {
@ -142,6 +179,7 @@ class LiveViewModel(
} else {
getRecommendChannelList()
}
getLatestPostListFromCreatorsYouFollow()
val liveNow = repository.roomList(
status = LiveRoomStatus.NOW,

View File

@ -5,11 +5,12 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemLiveNowBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.live.GetRoomListResponse
class LiveNowAdapter(
@ -23,13 +24,11 @@ class LiveNowAdapter(
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetRoomListResponse) {
binding.ivCover.load(item.coverImageUrl) {
binding.ivCover.loadUrl(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
}
binding.tvManager.text = item.creatorNickname
binding.tvNumberOfMembers.text = "${item.numberOfParticipate}"
binding.ivLock.visibility = if (item.isPrivateRoom) {
View.VISIBLE
} else {
@ -44,6 +43,21 @@ class LiveNowAdapter(
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_10_643bc8)
}
if (item.tags.isNotEmpty()) {
binding.tvTags.visibility = View.VISIBLE
binding.tvTags.text = item.tags.joinToString(" ") { "#$it" }
} else {
binding.tvTags.visibility = View.GONE
}
binding.tvTitle.text = item.title
binding.tvNickname.text = item.creatorNickname
binding.ivProfile.loadUrl(item.creatorProfileImage) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.root.setOnClickListener { onClick(item) }
}
}

View File

@ -22,6 +22,9 @@ import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment
import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog
import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog
import org.koin.android.ext.android.inject
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class LiveNowAllActivity : BaseActivity<ActivityLiveNowAllBinding>(
ActivityLiveNowAllBinding::inflate
@ -157,6 +160,15 @@ class LiveNowAllActivity : BaseActivity<ActivityLiveNowAllBinding>(
viewModel.enterRoom(roomId, onEnterRoomSuccess)
}
} else {
val beginDateFormat = SimpleDateFormat("yyyy.MM.dd EEE hh:mm a", Locale.ENGLISH)
val beginDate = beginDateFormat.parse(it.beginDateTime)!!
val now = Date()
val dateFormat = SimpleDateFormat("yyyy-MM-dd, HH:mm", Locale.getDefault())
val diffTime: Long = now.time - beginDate.time
val hours = (diffTime / (1000 * 60 * 60)).toInt()
val mins = (diffTime / (1000 * 60)).toInt() % 60
if (it.isPrivateRoom) {
LiveRoomPasswordDialog(
activity = this,
@ -174,8 +186,23 @@ class LiveNowAllActivity : BaseActivity<ActivityLiveNowAllBinding>(
LivePaymentDialog(
activity = this,
layoutInflater = layoutInflater,
title = "${it.price.moneyFormat()} 캔으로 입장",
desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.",
title = "유료 라이브 입장",
startDateTime = if (hours >= 1) {
dateFormat.format(beginDate)
} else {
null
},
nowDateTime = if (hours >= 1) {
dateFormat.format(now)
} else {
null
},
desc = "${it.price.moneyFormat()}캔을 차감하고\n라이브에 입장 하시겠습니까?",
desc2 = if (hours >= 1) {
"라이브를 시작한 지 ${hours}시간 ${mins}분이 지났습니다. 라이브에 입장 후 30분 이내에 라이브가 종료될 수도 있습니다."
} else {
null
},
confirmButtonTitle = "결제 후 입장",
confirmButtonClick = {
viewModel.enterRoom(roomId, onEnterRoomSuccess)

View File

@ -34,8 +34,6 @@ class LiveNowAllAdapter(
}
binding.tvNickname.text = item.creatorNickname
binding.tvTitle.text = item.title
binding.tvTotal.text = "/${item.numberOfPeople}"
binding.tvNumberOfParticipants.text = item.numberOfParticipate.toString()
binding.ivLock.visibility = if (item.isPrivateRoom) {
View.VISIBLE
} else {
@ -47,7 +45,7 @@ class LiveNowAllAdapter(
binding.tvAvailableParticipate.setTextColor(
ContextCompat.getColor(
context,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
} else {

View File

@ -26,8 +26,8 @@ import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
@ -55,6 +55,7 @@ import kr.co.vividnext.sodalive.common.SodaLiveService
import kr.co.vividnext.sodalive.databinding.ActivityLiveRoomBinding
import kr.co.vividnext.sodalive.dialog.LiveDialog
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.room.chat.LiveRoomChatAdapter
import kr.co.vividnext.sodalive.live.room.chat.LiveRoomChatRawMessage
@ -63,6 +64,7 @@ import kr.co.vividnext.sodalive.live.room.chat.LiveRoomDonationChat
import kr.co.vividnext.sodalive.live.room.chat.LiveRoomDonationStatusChat
import kr.co.vividnext.sodalive.live.room.chat.LiveRoomJoinChat
import kr.co.vividnext.sodalive.live.room.chat.LiveRoomNormalChat
import kr.co.vividnext.sodalive.live.room.chat.LiveRoomRouletteDonationChat
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageDialog
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageViewModel
@ -72,6 +74,9 @@ import kr.co.vividnext.sodalive.live.room.profile.LiveRoomProfileDialog
import kr.co.vividnext.sodalive.live.room.profile.LiveRoomProfileListAdapter
import kr.co.vividnext.sodalive.live.room.profile.LiveRoomUserProfileDialog
import kr.co.vividnext.sodalive.live.room.update.LiveRoomInfoEditDialog
import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewDialog
import kr.co.vividnext.sodalive.live.roulette.RouletteSpinDialog
import kr.co.vividnext.sodalive.live.roulette.config.RouletteConfigActivity
import kr.co.vividnext.sodalive.report.ProfileReportDialog
import kr.co.vividnext.sodalive.report.ReportType
import kr.co.vividnext.sodalive.report.UserReportDialog
@ -106,7 +111,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
private var isSpeakerMute = false
private var isMicrophoneMute = false
private var isSpeaker = false
private var isSpeakerFold = false
private var isNoChatting = false
private var remainingNoChattingTime = noChattingTime
@ -179,6 +183,27 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
}
private val rouletteConfigResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val isActiveRoulette = result.data?.getBooleanExtra(Constants.EXTRA_RESULT_ROULETTE, false)
if (resultCode == RESULT_OK && isActiveRoulette != null) {
agora.sendRawMessageToGroup(
rawMessage = Gson().toJson(
LiveRoomChatRawMessage(
type = LiveRoomChatRawMessageType.TOGGLE_ROULETTE,
message = "",
can = 0,
donationMessage = "",
isActiveRoulette = isActiveRoulette
)
).toByteArray()
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
agora = Agora(
context = this,
@ -328,8 +353,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
isStaff = {
viewModel.isEqualToManagerId(it.toInt())
},
onClickSendMessage = { userId, nickname ->
},
onClickSetManager = {
setManagerMessageToPeer(userId = it)
viewModel.setManager(roomId = roomId, userId = it) {
@ -430,33 +453,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
dialog.show(screenWidth)
}
binding.ivNotification.setOnClickListener { viewModel.toggleShowNotice() }
binding.rlNotice.setOnClickListener { viewModel.toggleExpandNotice() }
binding.tvSpeakerFold.setOnClickListener {
isSpeakerFold = !isSpeakerFold
if (isSpeakerFold) {
binding.rlSpeaker.visibility = View.VISIBLE
binding.rvSpeakers.visibility = View.VISIBLE
binding.tvSpeakerFold.text = "접기"
binding.tvSpeakerFold.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_live_detail_top,
0,
0,
0
)
} else {
binding.rlSpeaker.visibility = View.GONE
binding.rvSpeakers.visibility = View.GONE
binding.tvSpeakerFold.text = "펼치기"
binding.tvSpeakerFold.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_live_detail_bottom,
0,
0,
0
)
}
}
binding.tvNotification.setOnClickListener { viewModel.toggleShowNotice() }
binding.tvBgSwitch.setOnClickListener { viewModel.toggleBackgroundImage() }
binding.llDonation.setOnClickListener {
@ -551,7 +548,9 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
dialog.setPositiveButton("차단") { _, _ ->
roomUserProfileDialog.dismiss()
viewModel.memberBlock(userId) {
kickOut(userId)
if (viewModel.roomInfoResponse.creatorId == SharedPreferenceManager.userId) {
kickOut(userId)
}
}
}
dialog.setNegativeButton("취소") { _, _ -> }
@ -596,11 +595,11 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
binding.tvBgSwitch.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.tvBgSwitch
.setBackgroundResource(R.drawable.bg_round_corner_13_3_transparent_9970ff)
.setBackgroundResource(R.drawable.bg_round_corner_5_3_transparent_3bb9f1)
} else {
binding.ivCover.visibility = View.GONE
binding.tvBgSwitch.text = "배경 OFF"
@ -611,7 +610,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
)
)
binding.tvBgSwitch
.setBackgroundResource(R.drawable.bg_round_corner_13_3_transparent_bbbbbb)
.setBackgroundResource(R.drawable.bg_round_corner_5_3_transparent_bbbbbb)
}
}
@ -647,10 +646,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
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) {
@ -693,7 +688,11 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
speakerListAdapter.managerId = response.creatorId
speakerListAdapter.updateList(response.speakerList)
speakerListAdapter.updateList(
response.speakerList.filter {
it.id != response.creatorId
}
)
if (response.creatorId == SharedPreferenceManager.userId) {
binding.ivEdit.setOnClickListener {
@ -710,10 +709,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
setNoticeAndClickableUrl(binding.tvNotice, newContent)
if (newCoverImageUri != null) {
binding.ivCover.load(newCoverImageUri) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
}
binding.ivCover.load(newCoverImageUri)
}
agora.sendRawMessageToGroup(
@ -770,7 +766,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
setNoticeAndClickableUrl(binding.tvNotice, response.notice)
binding.tvCreatorNickname.text = response.creatorNickname
binding.ivCreatorProfile.load(response.creatorProfileUrl) {
binding.ivCreatorProfile.loadUrl(response.creatorProfileUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
@ -785,7 +781,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
if (response.creatorId != SharedPreferenceManager.userId) {
binding.ivCreatorFollow.visibility = View.VISIBLE
if (response.isFollowing) {
binding.ivCreatorFollow.setImageResource(R.drawable.btn_following)
binding.ivCreatorFollow.setImageResource(R.drawable.btn_select_checked)
binding.ivCreatorFollow.setOnClickListener {
viewModel.creatorUnFollow(
creatorId = response.creatorId,
@ -793,7 +789,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
)
}
} else {
binding.ivCreatorFollow.setImageResource(R.drawable.btn_follow)
binding.ivCreatorFollow.setImageResource(R.drawable.btn_plus_round)
binding.ivCreatorFollow.setOnClickListener {
viewModel.creatorFollow(
creatorId = response.creatorId,
@ -805,6 +801,12 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
binding.ivCreatorFollow.visibility = View.GONE
}
initRouletteSettingButton(isHost = response.creatorId == SharedPreferenceManager.userId)
activatingRouletteButton(
isHost = response.creatorId == SharedPreferenceManager.userId,
isActiveRoulette = response.isActiveRoulette
)
if (agora.rtmChannelIsNull()) {
joinChannel(response)
}
@ -812,25 +814,74 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
viewModel.isShowNotice.observe(this) {
if (it) {
binding.ivNotification.setImageResource(R.drawable.ic_notice_selected)
binding.rlNotice.visibility = View.VISIBLE
} else {
binding.ivNotification.setImageResource(R.drawable.ic_notice_normal)
binding.rlNotice.visibility = View.GONE
}
}
binding.tvNotification.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.tvNotification.setBackgroundResource(
R.drawable.bg_round_corner_5_3_transparent_3bb9f1
)
viewModel.isExpandNotice.observe(this) {
binding.tvNotice.maxLines = if (it) {
Int.MAX_VALUE
binding.llNotice.visibility = View.VISIBLE
} else {
1
binding.tvNotification.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_bbbbbb
)
)
binding.tvNotification.setBackgroundResource(
R.drawable.bg_round_corner_5_3_transparent_bbbbbb
)
binding.llNotice.visibility = View.GONE
}
}
viewModel.totalDonationCan.observe(this) {
binding.tvTotalCan.text = it.moneyFormat()
}
viewModel.coverImageUrlLiveData.observe(this) {
binding.ivCover.loadUrl(it)
}
}
private fun initRouletteSettingButton(isHost: Boolean) {
if (isHost) {
binding.flRouletteSettings.visibility = View.VISIBLE
binding.flRouletteSettings.setOnClickListener {
rouletteConfigResult.launch(
Intent(
applicationContext,
RouletteConfigActivity::class.java
)
)
}
} else {
binding.flRouletteSettings.visibility = View.GONE
}
}
private fun activatingRouletteButton(isHost: Boolean, isActiveRoulette: Boolean) {
if (!isHost && isActiveRoulette) {
binding.flRoulette.visibility = View.VISIBLE
binding.flRoulette.setOnClickListener {
viewModel.showRoulette {
RoulettePreviewDialog(
activity = this,
preview = it,
onClickSpin = { spinRoulette() },
layoutInflater = layoutInflater
).show()
}
}
} else {
binding.flRoulette.visibility = View.GONE
}
}
private fun setNoticeAndClickableUrl(textView: TextView, text: String) {
@ -937,7 +988,11 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
val rvSpeakers = binding.rvSpeakers
speakerListAdapter = LiveRoomProfileListAdapter()
rvSpeakers.layoutManager = GridLayoutManager(applicationContext, 5)
rvSpeakers.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.HORIZONTAL,
false
)
rvSpeakers.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
@ -947,8 +1002,8 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.top = 5f.dpToPx().toInt()
outRect.bottom = 5f.dpToPx().toInt()
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
})
rvSpeakers.adapter = speakerListAdapter
@ -1090,10 +1145,14 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
isMicrophoneMute = !isMicrophoneMute
agora.muteLocalAudioStream(isMicrophoneMute)
if (isMicrophoneMute) {
speakerListAdapter.muteSpeakers.add(SharedPreferenceManager.userId.toInt())
if (SharedPreferenceManager.userId == viewModel.roomInfoResponse.creatorId) {
setMuteSpeakerCreator(isMicrophoneMute)
} else {
speakerListAdapter.muteSpeakers.remove(SharedPreferenceManager.userId.toInt())
if (isMicrophoneMute) {
speakerListAdapter.muteSpeakers.add(SharedPreferenceManager.userId.toInt())
} else {
speakerListAdapter.muteSpeakers.remove(SharedPreferenceManager.userId.toInt())
}
}
}
@ -1190,6 +1249,46 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
}
private fun spinRoulette() {
viewModel.spinRoulette(roomId = roomId) { can, items, randomlySelectedItem ->
val rouletteRawMessage = Gson().toJson(
LiveRoomChatRawMessage(
type = LiveRoomChatRawMessageType.ROULETTE_DONATION,
message = randomlySelectedItem,
can = can,
donationMessage = "",
)
)
RouletteSpinDialog(
activity = this@LiveRoomActivity,
items = items,
selectedItem = randomlySelectedItem,
layoutInflater = layoutInflater
) {
agora.sendRawMessageToGroup(
rawMessage = rouletteRawMessage.toByteArray(),
onSuccess = {
handler.post {
chatAdapter.items.add(
LiveRoomRouletteDonationChat(
profileUrl = SharedPreferenceManager.profileImage,
nickname = SharedPreferenceManager.nickname,
rouletteResult = randomlySelectedItem
)
)
invalidateChat()
viewModel.addDonationCan(can)
}
},
onFailure = {
viewModel.refundRouletteDonation(roomId)
}
)
}.show()
}
}
private fun joinChannel(roomInfo: GetRoomInfoResponse) {
loadingDialog.show(width = screenWidth, message = "라이브에 입장하고 있습니다.")
@ -1261,6 +1360,31 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
invalidateChat()
}
}
LiveRoomChatRawMessageType.TOGGLE_ROULETTE -> {
handler.post {
activatingRouletteButton(
isHost = viewModel
.roomInfoResponse
.creatorId == SharedPreferenceManager.userId,
isActiveRoulette = rawMessage.isActiveRoulette ?: false
)
}
}
LiveRoomChatRawMessageType.ROULETTE_DONATION -> {
handler.post {
chatAdapter.items.add(
LiveRoomRouletteDonationChat(
profileUrl = profileUrl,
nickname = nickname,
rouletteResult = rawMessage.message
)
)
invalidateChat()
viewModel.addDonationCan(rawMessage.can)
}
}
}
} else {
val chat = message.text
@ -1338,6 +1462,14 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
}
private fun setMuteSpeakerCreator(isMute: Boolean) {
binding.ivMute.visibility = if (isMute) {
View.VISIBLE
} else {
View.GONE
}
}
private val rtcEventHandler = object : IRtcEngineEventHandler() {
@SuppressLint("NotifyDataSetChanged")
override fun onAudioVolumeIndication(
@ -1345,6 +1477,9 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
totalVolume: Int
) {
super.onAudioVolumeIndication(speakers, totalVolume)
Logger.e("onAudioVolumeIndication - $speakers")
val activeSpeakerIds = speakers
.asSequence()
.filter { it.volume > 0 }
@ -1353,13 +1488,17 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
Logger.e("onAudioVolumeIndication - $activeSpeakerIds")
handler.post {
speakerListAdapter.activeSpeakers.clear()
speakerListAdapter.activeSpeakers.addAll(activeSpeakerIds)
if (!activeSpeakerIds.contains(0)) {
speakerListAdapter.activeSpeakers.clear()
speakerListAdapter.activeSpeakers.addAll(activeSpeakerIds)
speakerListAdapter.notifyDataSetChanged()
if (activeSpeakerIds.contains(0) && !isMicrophoneMute) {
speakerListAdapter.activeSpeakers.add(SharedPreferenceManager.userId.toInt())
if (activeSpeakerIds.contains(viewModel.roomInfoResponse.creatorId.toInt())) {
binding.ivCreatorProfileBg.visibility = View.VISIBLE
} else {
binding.ivCreatorProfileBg.visibility = View.GONE
}
}
speakerListAdapter.notifyDataSetChanged()
}
}
@ -1389,12 +1528,16 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
override fun onUserMuteAudio(uid: Int, muted: Boolean) {
super.onUserMuteAudio(uid, muted)
handler.post {
if (muted) {
speakerListAdapter.muteSpeakers.add(uid)
if (uid == viewModel.roomInfoResponse.creatorId.toInt()) {
setMuteSpeakerCreator(muted)
} else {
speakerListAdapter.muteSpeakers.remove(uid)
if (muted) {
speakerListAdapter.muteSpeakers.add(uid)
} else {
speakerListAdapter.muteSpeakers.remove(uid)
}
speakerListAdapter.notifyDataSetChanged()
}
speakerListAdapter.notifyDataSetChanged()
}
Logger.e("onUserMuteAudio - uid: $uid, muted: $muted")
}

View File

@ -20,6 +20,11 @@ import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResp
import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse
import kr.co.vividnext.sodalive.live.room.profile.GetLiveRoomUserProfileResponse
import kr.co.vividnext.sodalive.live.room.update.EditLiveRoomInfoRequest
import kr.co.vividnext.sodalive.live.roulette.RouletteItem
import kr.co.vividnext.sodalive.live.roulette.RoulettePreview
import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewItem
import kr.co.vividnext.sodalive.live.roulette.RouletteRepository
import kr.co.vividnext.sodalive.live.roulette.SpinRouletteRequest
import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.report.ReportRequest
import kr.co.vividnext.sodalive.report.ReportType
@ -29,11 +34,13 @@ import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import kotlin.math.floor
class LiveRoomViewModel(
private val repository: LiveRepository,
private val userRepository: UserRepository,
private val reportRepository: ReportRepository
private val reportRepository: ReportRepository,
private val rouletteRepository: RouletteRepository
) : BaseViewModel() {
private val _roomInfoLiveData = MutableLiveData<GetRoomInfoResponse>()
val roomInfoLiveData: LiveData<GetRoomInfoResponse>
@ -43,14 +50,10 @@ class LiveRoomViewModel(
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _isShowNotice = MutableLiveData(true)
private val _isShowNotice = MutableLiveData(false)
val isShowNotice: LiveData<Boolean>
get() = _isShowNotice
private val _isExpandNotice = MutableLiveData(false)
val isExpandNotice: LiveData<Boolean>
get() = _isExpandNotice
private val _totalDonationCan = MutableLiveData(0)
val totalDonationCan: LiveData<Int>
get() = _totalDonationCan
@ -59,6 +62,10 @@ class LiveRoomViewModel(
val userProfileLiveData: LiveData<GetLiveRoomUserProfileResponse>
get() = _userProfileLiveData
private val _coverImageUrlLiveData = MutableLiveData("")
val coverImageUrlLiveData: LiveData<String>
get() = _coverImageUrlLiveData
lateinit var roomInfoResponse: GetRoomInfoResponse
fun isRoomInfoInitialized() = this::roomInfoResponse.isInitialized
@ -188,6 +195,10 @@ class LiveRoomViewModel(
Logger.e("data: ${it.data}")
_roomInfoLiveData.postValue(roomInfoResponse)
if (_coverImageUrlLiveData.value!! != roomInfoResponse.coverImageUrl) {
_coverImageUrlLiveData.value = roomInfoResponse.coverImageUrl
}
getTotalDonationCan(roomId = roomId)
if (userId > 0 && it.data.creatorId == SharedPreferenceManager.userId) {
@ -346,11 +357,6 @@ class LiveRoomViewModel(
fun toggleShowNotice() {
_isShowNotice.value = !isShowNotice.value!!
_isExpandNotice.value = false
}
fun toggleExpandNotice() {
_isExpandNotice.value = !isExpandNotice.value!!
}
fun toggleBackgroundImage() {
@ -775,4 +781,154 @@ class LiveRoomViewModel(
)
)
}
fun showRoulette(complete: (RoulettePreview) -> Unit) {
if (!_isLoading.value!!) {
_isLoading.value = true
compositeDisposable.add(
rouletteRepository.getRoulette(
creatorId = roomInfoResponse.creatorId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
val data = it.data
if (
it.success &&
data != null &&
data.isActive &&
data.items.isNotEmpty()
) {
complete(
RoulettePreview(
data.can,
items = calculatePercentages(data.items)
)
)
} else {
val message = it.message ?: "룰렛을 사용할 수 없습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("룰렛을 사용할 수 없습니다. 다시 시도해 주세요.")
}
)
)
}
}
fun spinRoulette(roomId: Long, complete: (Int, List<RouletteItem>, String) -> Unit) {
if (!_isLoading.value!!) {
_isLoading.value = true
compositeDisposable.add(
rouletteRepository.spinRoulette(
request = SpinRouletteRequest(roomId = roomId),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
val data = it.data
if (
it.success &&
data != null &&
data.isActive &&
data.items.isNotEmpty()
) {
SharedPreferenceManager.can -= data.can
randomSelectRouletteItem(data.can, data.items, complete)
} else {
val message = it.message ?: "룰렛을 사용할 수 없습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("룰렛을 사용할 수 없습니다. 다시 시도해 주세요.")
}
)
)
}
}
fun refundRouletteDonation(roomId: Long) {
_isLoading.postValue(true)
compositeDisposable.add(
rouletteRepository.refundRouletteDonation(
roomId,
"Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
_toastLiveData.postValue(
"후원에 실패했습니다.\n다시 후원해주세요.\n" +
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요."
)
}
)
)
}
private fun randomSelectRouletteItem(
can: Int,
items: List<RouletteItem>,
complete: (Int, List<RouletteItem>, String) -> Unit
) {
_isLoading.value = true
val rouletteItems = mutableListOf<String>()
items.asSequence().forEach { item ->
repeat(item.weight * 10) {
rouletteItems.add(item.title)
}
}
_isLoading.value = false
complete(can, items, rouletteItems.random())
}
private fun calculatePercentages(options: List<RouletteItem>): List<RoulettePreviewItem> {
val totalWeight = options.sumOf { it.weight }
val updatedOptions = options.asSequence().map { option ->
val percent = floor(option.weight.toDouble() / totalWeight * 10000) / 100
RoulettePreviewItem(
title = option.title,
percent = "${String.format("%.2f", percent)}%"
)
}.toList()
return updatedOptions
}
}

View File

@ -2,10 +2,12 @@ package kr.co.vividnext.sodalive.live.room.chat
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Typeface
import android.text.SpannableString
import android.text.Spanned
import android.text.TextUtils
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
@ -65,12 +67,7 @@ data class LiveRoomJoinChat(
)
spStr.setSpan(
CustomTypefaceSpan(
ResourcesCompat.getFont(
context,
R.font.gmarket_sans_bold
)
),
StyleSpan(Typeface.BOLD),
str.indexOf("'"),
str.indexOf("'님"),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
@ -174,6 +171,7 @@ data class LiveRoomNormalChat(
itemBinding.tvNickname.text = nickname
itemBinding.ivBg.visibility = View.VISIBLE
itemBinding.ivRoulette.visibility = View.GONE
itemBinding.tvCreatorOrManager.visibility = View.GONE
when (rank + 1) {
@ -190,7 +188,7 @@ data class LiveRoomNormalChat(
}
-1 -> {
itemBinding.ivBg.setImageResource(R.drawable.bg_circle_6f3dec_9970ff)
itemBinding.ivBg.setImageResource(R.drawable.bg_circle_3bb9f1)
itemBinding.ivCrown.setImageResource(R.drawable.ic_crown)
itemBinding.ivCrown.visibility = View.VISIBLE
}
@ -229,7 +227,7 @@ data class LiveRoomNormalChat(
)
if (SharedPreferenceManager.userId == userId) {
itemBinding.llMessageBg.setBackgroundResource(R.drawable.bg_round_corner_3_3_999970ff)
itemBinding.llMessageBg.setBackgroundResource(R.drawable.bg_round_corner_3_3_553bb9f1)
} else {
itemBinding.llMessageBg.setBackgroundResource(R.drawable.bg_round_corner_3_3_99000000)
}
@ -288,6 +286,7 @@ data class LiveRoomDonationChat(
itemBinding.ivCan.visibility = View.VISIBLE
itemBinding.ivBg.visibility = View.GONE
itemBinding.ivCrown.visibility = View.GONE
itemBinding.ivRoulette.visibility = View.GONE
itemBinding.tvCreatorOrManager.visibility = View.GONE
if (donationMessage.isNotBlank()) {
@ -302,35 +301,89 @@ data class LiveRoomDonationChat(
itemBinding.root.setBackgroundResource(
when {
can >= 100000 -> {
R.drawable.bg_round_corner_6_7_c25264
}
can >= 50000 -> {
R.drawable.bg_round_corner_6_7_e6d85e37
}
can >= 10000 -> {
R.drawable.bg_round_corner_6_7_e6d38c38
R.drawable.bg_round_corner_6_7_ccc25264
}
can >= 5000 -> {
R.drawable.bg_round_corner_6_7_e659548f
R.drawable.bg_round_corner_6_7_ccd85e37
}
can >= 1000 -> {
R.drawable.bg_round_corner_6_7_e64d6aa4
R.drawable.bg_round_corner_6_7_ccd38c38
}
can >= 500 -> {
R.drawable.bg_round_corner_6_7_e62d7390
R.drawable.bg_round_corner_6_7_cc59548f
}
can >= 100 -> {
R.drawable.bg_round_corner_6_7_cc4d6aa4
}
can >= 50 -> {
R.drawable.bg_round_corner_6_7_cc2d7390
}
else -> {
R.drawable.bg_round_corner_6_7_e6548f7d
R.drawable.bg_round_corner_6_7_cc548f7d
}
}
)
itemBinding.root.setPadding(33)
}
}
data class LiveRoomRouletteDonationChat(
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("nickname") val nickname: String,
@SerializedName("rouletteResult") val rouletteResult: String
) : LiveRoomChat() {
override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) {
val itemBinding = binding as ItemLiveRoomChatBinding
val chat = "[$rouletteResult] 당첨!"
val spChat = SpannableString(chat)
spChat.setSpan(
ForegroundColorSpan(
ContextCompat.getColor(
context,
R.color.color_ffe500
)
),
0,
chat.indexOf("]", 0, true) + 1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
val spNickname = SpannableString("${nickname}님의 룰렛 결과?")
spNickname.setSpan(
StyleSpan(Typeface.NORMAL),
0,
nickname.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
itemBinding.ivProfile.load(profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(23.3f.dpToPx()))
}
itemBinding.tvChat.text = spChat
itemBinding.tvNickname.text = spNickname
itemBinding.ivProfile.setOnClickListener {}
itemBinding.ivCan.visibility = View.GONE
itemBinding.ivBg.visibility = View.GONE
itemBinding.ivCrown.visibility = View.GONE
itemBinding.tvCreatorOrManager.visibility = View.GONE
itemBinding.ivRoulette.visibility = View.VISIBLE
itemBinding.tvDonationMessage.visibility = View.GONE
itemBinding.llMessageBg.setPadding(0)
itemBinding.llMessageBg.background = null
itemBinding.root.setBackgroundResource(R.drawable.bg_round_corner_6_7_ccc25264)
itemBinding.root.setPadding(33)
}
}

View File

@ -6,12 +6,21 @@ data class LiveRoomChatRawMessage(
@SerializedName("type") val type: LiveRoomChatRawMessageType,
@SerializedName("message") val message: String,
@SerializedName("can") val can: Int,
@SerializedName("donationMessage") val donationMessage: String?
@SerializedName("donationMessage") val donationMessage: String?,
@SerializedName("isActiveRoulette") val isActiveRoulette: Boolean? = null
)
enum class LiveRoomChatRawMessageType {
@SerializedName("DONATION") DONATION,
@SerializedName("SET_MANAGER") SET_MANAGER,
@SerializedName("EDIT_ROOM_INFO") EDIT_ROOM_INFO,
@SerializedName("DONATION_STATUS") DONATION_STATUS
@SerializedName("DONATION")
DONATION,
@SerializedName("SET_MANAGER")
SET_MANAGER,
@SerializedName("EDIT_ROOM_INFO")
EDIT_ROOM_INFO,
@SerializedName("DONATION_STATUS")
DONATION_STATUS,
@SerializedName("TOGGLE_ROULETTE")
TOGGLE_ROULETTE,
@SerializedName("ROULETTE_DONATION")
ROULETTE_DONATION
}

View File

@ -365,9 +365,9 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
binding.llReservationDatetime.visibility = View.GONE
binding.ivTimeReservation.visibility = View.GONE
binding.ivTimeNow.visibility = View.VISIBLE
binding.llTimeNow.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llTimeNow.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.llTimeReservation.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734
R.drawable.bg_round_corner_6_7_13181b
)
binding.tvTimeNow.setTextColor(
ContextCompat.getColor(
@ -379,22 +379,22 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
binding.tvTimeReservation.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
} else {
binding.llReservationDatetime.visibility = View.VISIBLE
binding.ivTimeReservation.visibility = View.VISIBLE
binding.ivTimeNow.visibility = View.GONE
binding.llTimeNow.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.llTimeNow.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.llTimeReservation.setBackgroundResource(
R.drawable.bg_round_corner_6_7_9970ff
R.drawable.bg_round_corner_6_7_3bb9f1
)
binding.tvTimeNow.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
@ -413,8 +413,8 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
binding.ivPrivate.visibility = View.VISIBLE
binding.ivOpen.visibility = View.GONE
binding.llPrivate.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llOpen.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.llPrivate.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.llOpen.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvPrivate.setTextColor(
ContextCompat.getColor(
@ -426,7 +426,7 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
binding.tvOpen.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
@ -437,13 +437,13 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
binding.ivOpen.visibility = View.VISIBLE
binding.ivPrivate.visibility = View.GONE
binding.llOpen.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llPrivate.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.llOpen.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.llPrivate.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvPrivate.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
@ -501,16 +501,16 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
viewModel.isAdultLiveData.observe(this) {
if (it) {
binding.ivAgeAll.visibility = View.GONE
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.ivAge19.visibility = View.VISIBLE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
@ -519,16 +519,16 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
)
} else {
binding.ivAge19.visibility = View.GONE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.ivAgeAll.visibility = View.VISIBLE
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
@ -605,7 +605,7 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
priceView.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
priceView.typeface = ResourcesCompat.getFont(

View File

@ -20,7 +20,9 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentLiveRoomDetailBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.convertDateFormat
import org.koin.android.ext.android.inject
import java.util.Locale
class LiveRoomDetailFragment(
private val roomId: Long,
@ -90,7 +92,11 @@ class LiveRoomDetailFragment(
}
binding.tvTitle.text = response.title
binding.tvDate.text = response.beginDateTime
binding.tvDate.text = response.beginDateTime.convertDateFormat(
from = "yyyy.MM.dd EEE hh:mm a",
to = "yyyy년 MM월 dd일 (E) a hh시 mm분",
inputLocale = Locale.ENGLISH
)
if (response.price > 0) {
binding.tvCan.text = response.price.toString()

View File

@ -1,32 +1,87 @@
package kr.co.vividnext.sodalive.live.room.dialog
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.widget.LinearLayout
import kr.co.vividnext.sodalive.dialog.LiveDialog
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import kr.co.vividnext.sodalive.databinding.DialogLivePaymentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class LivePaymentDialog(
activity: Activity,
layoutInflater: LayoutInflater,
title: String,
desc: String,
desc2: String? = null,
startDateTime: String? = null,
nowDateTime: String? = null,
confirmButtonTitle: String,
confirmButtonClick: () -> Unit,
cancelButtonTitle: String = "",
cancelButtonClick: (() -> Unit)? = null,
) : LiveDialog(
activity,
layoutInflater,
title,
desc,
confirmButtonTitle,
confirmButtonClick,
cancelButtonTitle,
cancelButtonClick
) {
private val alertDialog: AlertDialog
private val dialogView = DialogLivePaymentBinding.inflate(layoutInflater)
init {
val lp = dialogView.tvConfirm.layoutParams as LinearLayout.LayoutParams
lp.weight = 2F
dialogView.tvConfirm.layoutParams = lp
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialogView.tvTitle.text = title
dialogView.tvDesc.text = desc
if (startDateTime != null && nowDateTime != null) {
dialogView.tvDesc2.text = desc2
dialogView.tvNowDate.text = nowDateTime
dialogView.tvStartDate.text = startDateTime
dialogView.tvDesc2.visibility = View.VISIBLE
dialogView.llTimeNotice.visibility = View.VISIBLE
} else {
dialogView.tvDesc2.visibility = View.GONE
dialogView.llTimeNotice.visibility = View.GONE
}
dialogView.tvCancel.text = cancelButtonTitle
dialogView.tvCancel.setOnClickListener {
alertDialog.dismiss()
cancelButtonClick?.let { it() }
}
dialogView.tvConfirm.text = confirmButtonTitle
dialogView.tvConfirm.setOnClickListener {
alertDialog.dismiss()
confirmButtonClick()
}
dialogView.tvCancel.visibility = if (cancelButtonTitle.isNotBlank()) {
View.VISIBLE
} else {
View.GONE
}
dialogView.tvConfirm.visibility = if (confirmButtonTitle.isNotBlank()) {
View.VISIBLE
} else {
View.GONE
}
}
fun show(width: Int, message: String = "") {
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

@ -21,6 +21,7 @@ data class GetRoomInfoResponse(
@SerializedName("listenerList") val listenerList: List<LiveRoomMember>,
@SerializedName("managerList") val managerList: List<LiveRoomMember>,
@SerializedName("donationRankingTop3UserIds") val donationRankingTop3UserIds: List<Long>,
@SerializedName("isActiveRoulette") val isActiveRoulette: Boolean,
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
@SerializedName("password") val password: String? = null
)

View File

@ -5,11 +5,11 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.live.room.info.LiveRoomMember
class LiveRoomProfileListAdapter : RecyclerView.Adapter<LiveRoomProfileListAdapter.ViewHolder>() {
@ -17,10 +17,10 @@ class LiveRoomProfileListAdapter : RecyclerView.Adapter<LiveRoomProfileListAdapt
private val binding: ItemLiveRoomProfileBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LiveRoomMember) {
binding.ivProfile.load(item.profileImage) {
binding.ivProfile.loadUrl(item.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(23.3f.dpToPx()))
transformations(CircleCropTransformation())
if (activeSpeakers.contains(item.id.toInt())) {
binding.ivBg.visibility = View.VISIBLE
@ -35,15 +35,9 @@ class LiveRoomProfileListAdapter : RecyclerView.Adapter<LiveRoomProfileListAdapt
}
val ivMuteLp = binding.ivMute.layoutParams
ivMuteLp.width = 51.7f.dpToPx().toInt()
ivMuteLp.height = 51.7f.dpToPx().toInt()
ivMuteLp.width = 30f.dpToPx().toInt()
ivMuteLp.height = 30f.dpToPx().toInt()
binding.ivMute.layoutParams = ivMuteLp
if (managerId == item.id) {
binding.ivCrown.visibility = View.VISIBLE
} else {
binding.ivCrown.visibility = View.GONE
}
}
binding.tvNickname.text = item.nickname

View File

@ -20,7 +20,6 @@ class LiveRoomUserProfileDialog(
private val userProfileLiveData: LiveData<GetLiveRoomUserProfileResponse>,
layoutInflater: LayoutInflater,
private val isStaff: (Long) -> Boolean,
private val onClickSendMessage: (Long, String) -> Unit,
private val onClickSetManager: (Long) -> Unit,
private val onClickReleaseManager: (Long) -> Unit,
private val onClickFollow: (Long) -> Unit,
@ -84,18 +83,8 @@ class LiveRoomUserProfileDialog(
}
dialogView.tvNickname.text = userProfile.nickname
dialogView.tvTags.text = userProfile.tags
dialogView.tvGender.text = userProfile.gender
dialogView.tvIntroduce.text = userProfile.introduce
dialogView.ivClose.setOnClickListener { alertDialog.dismiss() }
dialogView.llSendMessage.setOnClickListener {
onClickSendMessage(
userProfile.userId,
userProfile.nickname
)
}
dialogView.tvIntroduce.setOnClickListener {
isIntroduceFold = !isIntroduceFold
@ -169,25 +158,33 @@ class LiveRoomUserProfileDialog(
}
if (userProfile.isFollowing != null) {
dialogView.llFollow.visibility = View.VISIBLE
dialogView.ivFollow.visibility = View.VISIBLE
if (userProfile.isFollowing) {
dialogView.tvFollow.text = "팔로잉"
dialogView.ivFollow.setImageResource(R.drawable.ic_alarm_selected)
dialogView.llFollow.setBackgroundResource(
R.drawable.bg_round_corner_23_3_3e1b93_9970ff
)
dialogView.tvFollow.setOnClickListener { onClickUnFollow(userProfile.userId) }
dialogView.ivFollow.setImageResource(R.drawable.btn_following)
dialogView.ivFollow.setOnClickListener { onClickUnFollow(userProfile.userId) }
} else {
dialogView.tvFollow.text = "팔로우"
dialogView.ivFollow.setImageResource(R.drawable.ic_alarm)
dialogView.llFollow.setBackgroundResource(
R.drawable.bg_round_corner_23_3_transparent_9970ff
)
dialogView.tvFollow.setOnClickListener { onClickFollow(userProfile.userId) }
dialogView.ivFollow.setImageResource(R.drawable.btn_follow)
dialogView.ivFollow.setOnClickListener { onClickFollow(userProfile.userId) }
}
if (userProfile.tags.isNotBlank()) {
dialogView.tvTags.text = userProfile.tags
dialogView.tvTags.visibility = View.VISIBLE
} else {
dialogView.tvTags.visibility = View.GONE
}
if (userProfile.introduce.isNotBlank()) {
dialogView.tvIntroduce.text = userProfile.introduce
dialogView.tvIntroduce.visibility = View.VISIBLE
} else {
dialogView.tvIntroduce.visibility = View.GONE
}
} else {
dialogView.llFollow.visibility = View.GONE
dialogView.tvTags.visibility = View.GONE
dialogView.ivFollow.visibility = View.GONE
dialogView.tvIntroduce.visibility = View.GONE
}
dialogView.ivMenu.setOnClickListener {

View File

@ -0,0 +1,14 @@
package kr.co.vividnext.sodalive.live.roulette
import com.google.gson.annotations.SerializedName
data class GetRouletteResponse(
@SerializedName("can") val can: Int,
@SerializedName("isActive") val isActive: Boolean,
@SerializedName("items") val items: List<RouletteItem>
)
data class RouletteItem(
@SerializedName("title") val title: String,
@SerializedName("weight") val weight: Int
)

View File

@ -0,0 +1,45 @@
package kr.co.vividnext.sodalive.live.roulette
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.View
class RouletteInvertedTriangle @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val trianglePath = Path()
private var trianglePaint = Paint()
private val triangleSize = 60f
init {
trianglePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.RED // Set the color of the triangle
style = Paint.Style.FILL // Fill the triangle
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Clear the old path
trianglePath.reset()
// Define the new path for the inverted triangle
// Starting point (top of the triangle)
trianglePath.moveTo((width / 2f) - 30, -10f)
// Line to bottom left of the triangle
trianglePath.lineTo((width / 2f) + 30, -10f)
// Line to bottom right of the triangle
trianglePath.lineTo(width / 2f, 10f + triangleSize)
// Close the path to form a triangle
trianglePath.close()
canvas.drawPath(trianglePath, trianglePaint)
}
}

View File

@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.live.roulette
data class RoulettePreview(
val can: Int,
val items: List<RoulettePreviewItem>
)
data class RoulettePreviewItem(
val title: String,
val percent: String
)

View File

@ -0,0 +1,106 @@
package kr.co.vividnext.sodalive.live.roulette
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.DialogRoulettePreviewBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
class RoulettePreviewDialog(
private val activity: FragmentActivity,
private val preview: RoulettePreview,
private val title: String = "",
private val onClickSpin: (() -> Unit)? = null,
layoutInflater: LayoutInflater
) {
private val alertDialog: AlertDialog
private val dialogView = DialogRoulettePreviewBinding.inflate(layoutInflater)
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
setupView()
}
fun show() {
alertDialog.show()
val lp = WindowManager.LayoutParams()
lp.copyFrom(alertDialog.window?.attributes)
lp.width = activity.resources.displayMetrics.widthPixels - (26.7f.dpToPx()).toInt()
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
alertDialog.window?.attributes = lp
}
@SuppressLint("SetTextI18n")
private fun setupView() {
dialogView.tvCancel.setOnClickListener { alertDialog.dismiss() }
dialogView.tvSpinRoulette.text = "${preview.can}캔으로 룰렛 돌리기"
dialogView.tvSpinRoulette.setOnClickListener {
if (onClickSpin != null) {
onClickSpin!!()
}
alertDialog.dismiss()
}
dialogView.tvTitle.text = title.ifBlank { "룰렛" }
if (onClickSpin != null) {
dialogView.tvCan.visibility = View.VISIBLE
dialogView.tvCan.text = SharedPreferenceManager.can.moneyFormat()
dialogView.tvCan.setOnClickListener {
alertDialog.dismiss()
val intent = Intent(activity, CanChargeActivity::class.java)
intent.putExtra(Constants.EXTRA_GO_TO_PREV_PAGE, true)
activity.startActivity(intent)
}
} else {
dialogView.tvCan.visibility = View.GONE
}
preview.items.forEachIndexed { index, item ->
dialogView.llRouletteOptionContainer.addView(createOptionView(index, item))
}
}
@SuppressLint("SetTextI18n")
private fun createOptionView(index: Int, item: RoulettePreviewItem): View {
val itemView = LayoutInflater
.from(activity)
.inflate(
R.layout.layout_roulette_preview_item,
dialogView.llRouletteOptionContainer,
false
)
val tvItemTitle = itemView.findViewById<TextView>(R.id.tv_roulette_item_title)
val tvOrder = itemView.findViewById<TextView>(R.id.tv_order)
tvItemTitle.text = "${item.title} (${item.percent})"
tvOrder.text = "${index + 1}"
return itemView
}
}

View File

@ -0,0 +1,33 @@
package kr.co.vividnext.sodalive.live.roulette
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.live.roulette.config.CreateOrUpdateRouletteRequest
import kr.co.vividnext.sodalive.live.roulette.config.RouletteApi
class RouletteRepository(private val api: RouletteApi) {
fun createOrUpdateRoulette(
request: CreateOrUpdateRouletteRequest,
token: String
) = api.createOrUpdateRoulette(
request = request,
authHeader = token
)
fun getRoulette(creatorId: Long, token: String) = api.getRoulette(
creatorId = creatorId,
authHeader = token
)
fun spinRoulette(request: SpinRouletteRequest, token: String) = api.spinRoulette(
request = request,
authHeader = token
)
fun refundRouletteDonation(roomId: Long, token: String): Single<ApiResponse<Any>> {
return api.refundRouletteDonation(
id = roomId,
authHeader = token
)
}
}

View File

@ -0,0 +1,62 @@
package kr.co.vividnext.sodalive.live.roulette
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.Window
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
import kr.co.vividnext.sodalive.databinding.DialogRouletteSpinBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class RouletteSpinDialog(
private val activity: FragmentActivity,
private val items: List<RouletteItem>,
private val selectedItem: String,
layoutInflater: LayoutInflater,
private val complete: () -> Unit
) {
private val alertDialog: AlertDialog
private val dialogView = DialogRouletteSpinBinding.inflate(layoutInflater)
private val handler = Handler(Looper.getMainLooper())
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
setupView()
}
fun show() {
alertDialog.show()
val lp = WindowManager.LayoutParams()
lp.copyFrom(alertDialog.window?.attributes)
lp.width = activity.resources.displayMetrics.widthPixels - (26.7f.dpToPx()).toInt()
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
alertDialog.window?.attributes = lp
rotateToOption()
}
private fun setupView() {
dialogView.roulette.items = items
}
private fun rotateToOption() {
dialogView.roulette.rotateToOption(selectedItem) {
handler.postDelayed({
alertDialog.dismiss()
complete()
}, 1500)
}
}
}

View File

@ -0,0 +1,158 @@
package kr.co.vividnext.sodalive.live.roulette
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.view.animation.Animation
import android.view.animation.RotateAnimation
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin
class RouletteView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var rect = RectF()
private val fillPaint = Paint()
private val textPaint = Paint()
private val strokePaint = Paint()
var items = listOf<RouletteItem>()
private val colors = listOf(
Color.parseColor("#D73535"),
Color.parseColor("#FF5151"),
Color.parseColor("#FF7C32"),
Color.parseColor("#FFAF13"),
Color.parseColor("#FFC658"),
Color.parseColor("#8BDA70"),
Color.parseColor("#06AB97"),
Color.parseColor("#12AAFF"),
Color.parseColor("#0052B3"),
Color.parseColor("#7444FF")
)
init {
strokePaint.apply {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 10f
isAntiAlias = true
}
fillPaint.apply {
style = Paint.Style.FILL
isAntiAlias = true
}
textPaint.apply {
color = Color.WHITE
textSize = 30f
textAlign = Paint.Align.CENTER
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
val diameter = min(width, height) - strokePaint.strokeWidth
rect.set(
0f + strokePaint.strokeWidth / 2,
0f + strokePaint.strokeWidth / 2,
diameter,
diameter
)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val totalWeight = items.asSequence().map { it.weight }.sum()
var startAngle = -90f
val shuffledColors = colors.shuffled()
items.forEachIndexed { index, (option, weight) ->
val sweepAngle = (weight / totalWeight.toFloat()) * 360f
fillPaint.color = shuffledColors[index]
canvas.drawArc(rect, startAngle, sweepAngle, true, fillPaint)
drawOptionText(canvas, option, startAngle, sweepAngle)
startAngle += sweepAngle
}
canvas.drawCircle(rect.centerX(), rect.centerY(), rect.width() / 2, strokePaint)
}
private fun drawOptionText(
canvas: Canvas,
option: String,
startAngle: Float,
sweepAngle: Float
) {
val textRadius = rect.width() / 4 // Increase radius to move text outside the circle
val angle = Math.toRadians((startAngle + sweepAngle / 2).toDouble()).toFloat()
// Calculate the text position
val x = rect.centerX() + textRadius * cos(angle) + 10
val y = rect.centerY() + textRadius * sin(angle)
// Save the canvas state
val saveCount = canvas.save()
// Rotate the canvas around the text position
canvas.rotate(startAngle + sweepAngle / 2, x, y)
// Draw the text aligned with the segment
canvas.drawText(option, x, y, textPaint)
// Restore the canvas to its previous state
canvas.restoreToCount(saveCount)
}
private fun getAngleForOption(option: String): Float {
val totalWeight = items.asSequence().map { it.weight }.sum()
var startAngle = 0f
items.forEach { (currentOption, weight) ->
val sweepAngle = (weight / totalWeight.toFloat()) * 360f
if (currentOption == option) {
// Return the midpoint angle of the segment
return (startAngle + sweepAngle / 2)
}
startAngle += sweepAngle
}
return 0f
}
fun rotateToOption(option: String, complete: () -> Unit) {
val targetAngle = 0 - (getAngleForOption(option) + 360 * 10)
val rotateAnimation = RotateAnimation(
0f, targetAngle,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f
)
rotateAnimation.duration = 2000
rotateAnimation.fillAfter = true
rotateAnimation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation?) {
}
override fun onAnimationEnd(animation: Animation?) {
complete()
}
override fun onAnimationRepeat(animation: Animation?) {
}
})
startAnimation(rotateAnimation)
}
}

View File

@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.live.roulette
import com.google.gson.annotations.SerializedName
data class SpinRouletteRequest(
@SerializedName("roomId") val roomId: Long,
@SerializedName("container") val container: String = "aos"
)

View File

@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.live.roulette.config
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.live.roulette.RouletteItem
data class CreateOrUpdateRouletteRequest(
@SerializedName("can") val can: Int,
@SerializedName("isActive") val isActive: Boolean,
@SerializedName("items") val items: List<RouletteItem>
)

View File

@ -0,0 +1,38 @@
package kr.co.vividnext.sodalive.live.roulette.config
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.live.roulette.GetRouletteResponse
import kr.co.vividnext.sodalive.live.roulette.SpinRouletteRequest
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
interface RouletteApi {
@POST("/roulette")
fun createOrUpdateRoulette(
@Body request: CreateOrUpdateRouletteRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/roulette")
fun getRoulette(
@Query("creatorId") creatorId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetRouletteResponse>>
@POST("/roulette/spin")
fun spinRoulette(
@Body request: SpinRouletteRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetRouletteResponse>>
@POST("/roulette/refund/{id}")
fun refundRouletteDonation(
@Path("id") id: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
}

View File

@ -0,0 +1,36 @@
package kr.co.vividnext.sodalive.live.roulette.config
import android.os.Bundle
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.databinding.ActivityRouletteConfigBinding
class RouletteConfigActivity : BaseActivity<ActivityRouletteConfigBinding>(
ActivityRouletteConfigBinding::inflate
) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
changeFragment()
}
override fun setupView() {
binding.toolbar.tvBack.text = "룰렛설정"
binding.toolbar.tvBack.setOnClickListener { finish() }
}
private fun changeFragment(tag: String = "settings") {
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
val currentFragment = fragmentManager.primaryNavigationFragment
if (currentFragment != null) {
fragmentTransaction.hide(currentFragment)
}
val fragment = RouletteSettingsFragment()
fragmentTransaction.add(R.id.container, fragment, tag)
fragmentTransaction.setPrimaryNavigationFragment(fragment)
fragmentTransaction.setReorderingAllowed(true)
fragmentTransaction.commitNow()
}
}

View File

@ -0,0 +1,3 @@
package kr.co.vividnext.sodalive.live.roulette.config
data class RouletteOption(var title: String, var weight: Int, var percentage: String = "50.00")

View File

@ -0,0 +1,188 @@
package kr.co.vividnext.sodalive.live.roulette.config
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Service
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.InputFilter
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentRouletteSettingsBinding
import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewDialog
import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit
class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
FragmentRouletteSettingsBinding::inflate
) {
private val viewModel: RouletteSettingsViewModel by inject()
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
private val handler = Handler(Looper.getMainLooper())
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
bindData()
viewModel.getRoulette()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireActivity().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
binding.etSetPrice.filters = arrayOf(InputFilter { source, start, end, _, _, _ ->
// Only allow numeric input
for (i in start until end) {
if (!Character.isDigit(source[i])) {
return@InputFilter ""
}
}
null
})
binding.ivRouletteIsActive.setOnClickListener { viewModel.toggleIsActive() }
binding.ivAddOption.setOnClickListener { addOption() }
binding.tvPreview.setOnClickListener {
handler.postDelayed({
imm.hideSoftInputFromWindow(view?.windowToken, 0)
}, 100)
viewModel.onClickPreview()
}
binding.tvSave.setOnClickListener { _ ->
handler.postDelayed({
imm.hideSoftInputFromWindow(view?.windowToken, 0)
}, 100)
viewModel.createOrUpdateRoulette {
val resultIntent = Intent().apply { putExtra(Constants.EXTRA_RESULT_ROULETTE, it) }
requireActivity().setResult(Activity.RESULT_OK, resultIntent)
requireActivity().finish()
}
}
}
private fun bindData() {
viewModel.isActiveLiveData.observe(viewLifecycleOwner) {
binding.ivRouletteIsActive.setImageResource(
if (it) R.drawable.btn_toggle_on_big else R.drawable.btn_toggle_off_big
)
}
viewModel.optionsLiveData.observe(viewLifecycleOwner) { updateOptionUi(it) }
viewModel.toastLiveData.observe(viewLifecycleOwner) { it?.let { showToast(it) } }
viewModel.canLiveData.observe(viewLifecycleOwner) {
if (it > 0) {
binding.etSetPrice.setText("$it")
} else {
binding.etSetPrice.setText("")
}
}
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.roulettePreviewLiveData.observe(viewLifecycleOwner) {
RoulettePreviewDialog(
activity = requireActivity(),
preview = it,
title = "룰렛 미리보기",
layoutInflater = layoutInflater
).show()
}
compositeDisposable.add(
binding.etSetPrice.textChanges().skip(1)
.debounce(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
if (it.trim().isNotEmpty()) {
viewModel.can = it.toString().toInt()
}
}
)
}
private fun addOption() {
val newOption = RouletteOption("", 1)
viewModel.addOption(newOption)
}
private fun updateOptionUi(options: List<RouletteOption>) {
binding.llRouletteOptionContainer.removeAllViews()
options.forEachIndexed { index, option ->
binding.llRouletteOptionContainer.addView(createOptionView(index, option))
}
}
@SuppressLint("SetTextI18n")
private fun createOptionView(index: Int, option: RouletteOption): View {
val optionView = LayoutInflater
.from(context)
.inflate(
R.layout.layout_roulette_option,
binding.llRouletteOptionContainer,
false
)
val etOption = optionView.findViewById<EditText>(R.id.et_option)
val tvOptionTitle = optionView.findViewById<TextView>(R.id.tv_option_title)
val tvPercentage = optionView.findViewById<TextView>(R.id.tv_option_percentage)
val ivMinus = optionView.findViewById<ImageView>(R.id.iv_minus)
val ivPlus = optionView.findViewById<ImageView>(R.id.iv_plus)
val tvDelete = optionView.findViewById<TextView>(R.id.tv_delete)
etOption.setText(option.title)
tvOptionTitle.text = "옵션 ${index + 1}"
tvPercentage.text = "${option.percentage}%"
ivMinus.setOnClickListener { viewModel.subtractWeight(index) }
ivPlus.setOnClickListener { viewModel.plusWeight(index) }
if (index == 0 || index == 1) {
tvDelete.visibility = View.GONE
} else {
tvDelete.visibility = View.VISIBLE
tvDelete.setOnClickListener { viewModel.deleteOption(index) }
}
compositeDisposable.add(
etOption.textChanges().skip(1)
.debounce(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.inputOption(index, it.toString())
}
)
return optionView
}
}

View File

@ -0,0 +1,234 @@
package kr.co.vividnext.sodalive.live.roulette.config
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.live.roulette.RouletteItem
import kr.co.vividnext.sodalive.live.roulette.RoulettePreview
import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewItem
import kr.co.vividnext.sodalive.live.roulette.RouletteRepository
import kotlin.math.floor
class RouletteSettingsViewModel(private val repository: RouletteRepository) : BaseViewModel() {
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _optionsLiveData = MutableLiveData<List<RouletteOption>>(listOf())
val optionsLiveData: LiveData<List<RouletteOption>>
get() = _optionsLiveData
private val _isActiveLiveData = MutableLiveData(false)
val isActiveLiveData: LiveData<Boolean>
get() = _isActiveLiveData
private val _canLiveData = MutableLiveData(0)
val canLiveData: LiveData<Int>
get() = _canLiveData
private val _roulettePreviewLiveData = MutableLiveData<RoulettePreview>()
val roulettePreviewLiveData: LiveData<RoulettePreview>
get() = _roulettePreviewLiveData
private val options = mutableListOf<RouletteOption>()
var can = 0
var isActive = false
fun plusWeight(optionIndex: Int) {
val currentOption = options[optionIndex]
options[optionIndex] = currentOption.copy(weight = currentOption.weight + 1)
recalculatePercentages(options)
}
fun subtractWeight(optionIndex: Int) {
if (options[optionIndex].weight > 1) {
val currentOption = options[optionIndex]
options[optionIndex] = currentOption.copy(weight = currentOption.weight - 1)
recalculatePercentages(options)
}
}
fun addOption(newOption: RouletteOption) {
if (options.size >= 10) return
options.add(newOption)
recalculatePercentages(options)
}
fun deleteOption(index: Int) {
val updatedOptions = options.filterIndexed { currentIndex, _ -> currentIndex != index }
removeAllAndAddOptions(updatedOptions)
recalculatePercentages(updatedOptions)
}
fun inputOption(optionIndex: Int, title: String) {
val currentOption = options[optionIndex]
options[optionIndex] = currentOption.copy(title = title)
}
private fun recalculatePercentages(options: List<RouletteOption>) {
val totalWeight = options.sumOf { it.weight }
val updatedOptions = options.asSequence().map { option ->
val percent = floor(option.weight.toDouble() / totalWeight * 10000) / 100
option.copy(percentage = String.format("%.2f", percent))
}.toList()
removeAllAndAddOptions(updatedOptions)
_optionsLiveData.value = updatedOptions
}
fun toggleIsActive() {
isActive = !isActive
_isActiveLiveData.postValue(isActive)
}
fun onClickPreview() {
_isLoading.value = true
val items = mutableListOf<RoulettePreviewItem>()
for (option in options) {
if (option.title.trim().isEmpty()) {
_toastLiveData.value = "옵션은 빈칸을 할 수 없습니다."
_isLoading.value = false
return
}
items.add(RoulettePreviewItem(option.title, "${option.percentage}%"))
}
_roulettePreviewLiveData.postValue(RoulettePreview(can, items))
_isLoading.value = false
}
fun createOrUpdateRoulette(onSuccess: (Boolean) -> Unit) {
if (!_isLoading.value!!) {
_isLoading.value = true
val items = mutableListOf<RouletteItem>()
for (option in options) {
if (option.title.trim().isEmpty()) {
_toastLiveData.value = "옵션은 빈칸을 할 수 없습니다."
_isLoading.value = false
return
}
items.add(RouletteItem(title = option.title, weight = option.weight))
}
val request = CreateOrUpdateRouletteRequest(
can = can,
isActive = isActive,
items = items
)
compositeDisposable.add(
repository.createOrUpdateRoulette(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null && it.data is Boolean) {
val message = if (it.data) {
"룰렛을 활성화 했습니다."
} else {
"룰렛을 비활성화 했습니다."
}
_toastLiveData.postValue(message)
onSuccess(it.data)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
fun getRoulette() {
if (!_isLoading.value!!) {
_isLoading.value = true
compositeDisposable.add(
repository.getRoulette(
creatorId = SharedPreferenceManager.userId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
val data = it.data
if (data != null && data.items.isNotEmpty()) {
_isActiveLiveData.value = data.isActive
_canLiveData.value = data.can
isActive = data.isActive
can = data.can
val options = data.items.asSequence().map { item ->
RouletteOption(title = item.title, weight = item.weight)
}.toList()
removeAllAndAddOptions(options = options)
recalculatePercentages(options)
} else {
_isActiveLiveData.value = false
_canLiveData.value = 0
isActive = false
can = 0
options.add(RouletteOption(title = "", weight = 1))
options.add(RouletteOption(title = "", weight = 1))
recalculatePercentages(options)
}
} 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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
private fun removeAllAndAddOptions(options: List<RouletteOption>) {
this.options.clear()
this.options.addAll(options)
}
}

View File

@ -19,8 +19,6 @@ import com.google.firebase.messaging.FirebaseMessaging
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.orhanobut.logger.Logger
import kr.co.pointclick.sdk.offerwall.core.PointClickAd
import kr.co.pointclick.sdk.offerwall.core.events.PackageReceiver
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
@ -52,11 +50,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
private val handler = Handler(Looper.getMainLooper())
private val audioContentReceiver = AudioContentReceiver()
private var packageReceiver: PackageReceiver? = null
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
executeDeeplink()
executeDeeplink(intent)
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -67,14 +63,17 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
getMemberInfo()
getEventPopup()
initPointClick()
handler.postDelayed({ executeDeeplink() }, 500)
handler.postDelayed({ executeDeeplink(intent) }, 500)
}
override fun onResume() {
super.onResume()
val intentFilter = IntentFilter(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
registerReceiver(audioContentReceiver, intentFilter)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(audioContentReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(audioContentReceiver, intentFilter)
}
startService(
Intent(this, AudioContentPlayService::class.java).apply {
@ -88,13 +87,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
super.onPause()
}
override fun onDestroy() {
if (packageReceiver != null) {
applicationContext.unregisterReceiver(packageReceiver)
}
super.onDestroy()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
liveFragment = LiveFragment()
@ -113,7 +105,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
setupBottomTabLayout()
}
private fun executeDeeplink() {
private fun executeDeeplink(intent: Intent) {
val bundle = intent.getBundleExtra(Constants.EXTRA_DATA)
if (bundle != null) {
try {
@ -374,25 +366,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
}
}
private fun initPointClick() {
try {
val intentFilter = IntentFilter()
intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
intentFilter.addDataScheme("package");
packageReceiver = PackageReceiver()
applicationContext.registerReceiver(packageReceiver, intentFilter)
} catch (e: Exception) {
e.printStackTrace()
}
PointClickAd.init(
"fc07cfb1-ef16-455c-bdad-22aa9e8fd78c",
SharedPreferenceManager.userId.toString()
)
}
inner class AudioContentReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)

View File

@ -187,36 +187,36 @@ class TextMessageFragment : BaseFragment<FragmentTextMessageBinding>(
when (it) {
MessageBox.SENT -> {
binding.tvSent.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_9970ff
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
)
binding.tvSent.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.color_9970ff
R.color.color_3bb9f1
)
)
}
MessageBox.RECEIVE -> {
binding.tvReceive.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_9970ff
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
)
binding.tvReceive.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.color_9970ff
R.color.color_3bb9f1
)
)
}
MessageBox.KEEP -> {
binding.tvKeep.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_9970ff
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
)
binding.tvKeep.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.color_9970ff
R.color.color_3bb9f1
)
)
}

View File

@ -94,7 +94,7 @@ class TextMessageWriteActivity : BaseActivity<ActivityTextMessageWriteBinding>(
private fun bindData() {
compositeDisposable.add(
binding.etMessage.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.debounce(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {

View File

@ -313,36 +313,36 @@ class VoiceMessageFragment : BaseFragment<FragmentVoiceMessageBinding>(
when (it) {
MessageBox.SENT -> {
binding.tvSent.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_9970ff
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
)
binding.tvSent.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.color_9970ff
R.color.color_3bb9f1
)
)
}
MessageBox.RECEIVE -> {
binding.tvReceive.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_9970ff
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
)
binding.tvReceive.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.color_9970ff
R.color.color_3bb9f1
)
)
}
MessageBox.KEEP -> {
binding.tvKeep.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_9970ff
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
)
binding.tvKeep.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.color_9970ff
R.color.color_3bb9f1
)
)
}

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