Compare commits

167 Commits

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

으로 변경
2023-08-14 11:15:21 +09:00
5e44949094 real agora app id 변경 2023-08-09 16:42:29 +09:00
bb1e260a4c 푸시메시지 터치 액션 추가 2023-08-09 13:52:26 +09:00
68f2896031 모든 기기에서 로그아웃 추가 2023-08-09 08:56:04 +09:00
a75217ee09 고객센터 페이지 추가 2023-08-09 08:39:24 +09:00
e8b4134956 라이브 예약 중 전체 보기 페이지 추가 2023-08-09 08:21:27 +09:00
035fa5a2fa 라이브 전체 보기 페이지 추가 2023-08-09 08:09:47 +09:00
8db80f2da9 추천 라이브 터치 액션 추가 2023-08-09 07:55:48 +09:00
6d93da2136 팔로잉 채널 전체보기 페이지 추가 2023-08-09 07:53:38 +09:00
f455aa81a2 라이브 메인 - 팔로잉 채널 API 연동 2023-08-09 07:27:38 +09:00
1b53aec571 푸시 - setSmallIcon 추가 2023-08-09 07:22:40 +09:00
6126525ebb PlaybackTracking DI 등록 2023-08-07 14:25:16 +09:00
0563fe6890 accountId -> memberId 이름 변경 2023-08-07 14:21:26 +09:00
1329ae5e5d 콘텐츠 기능 추가 2023-08-05 01:25:09 +09:00
7dbbd8d490 라이브 예약 현황, 예약 취소 추가 2023-08-02 23:56:45 +09:00
c5896529e1 내 채널 보기 버튼 액션 추가 2023-08-02 17:44:48 +09:00
ce35be9688 메시지 상세 페이지 추가 2023-08-02 17:38:48 +09:00
cc8fab76b0 설정 페이지 추가 2023-08-02 17:26:46 +09:00
3ef78b64ad 메시지 페이지 추가 2023-08-02 14:57:16 +09:00
14b652d38e 크리에이터 채널 페이지 추가 2023-08-01 15:10:33 +09:00
662ef64696 탐색 메인 페이지 추가 2023-08-01 10:29:49 +09:00
c2618669c8 라이브 방 추가 2023-08-01 07:04:16 +09:00
8a094adc4f 라이브 - 시작, 취소, 입장, 수정, 예약 기능 추가 2023-07-31 17:15:46 +09:00
0cbf2abf5e 라이브 방 상세 보기 추가 2023-07-30 21:16:16 +09:00
79127801c6 코인 충전, 코인 내역 2023-07-30 16:20:58 +09:00
7fb43b3f91 앱 아이콘 변경 2023-07-29 03:12:21 +09:00
3b235a8495 캔 충전/사용 내역 UI/API 추가 2023-07-29 03:12:09 +09:00
03de8eba86 마이페이지 메인 - 코인(캔) 아이콘 변경 2023-07-28 18:24:49 +09:00
bf7a7d69a2 본인인증 추가 2023-07-28 17:39:19 +09:00
8e0a5ccc91 마이페이지 메인 - UI, Api 적용 2023-07-28 15:01:48 +09:00
bad5e6612a 라이브 메인 - UI, Api 적용 2023-07-27 06:46:26 +09:00
6f86663a54 FCM 설정
FCM 토큰 업데이트 API 적용
2023-07-25 03:20:42 +09:00
fd8c4e726d Firebase 추가
Crashlytics 추가
RemoteConfig 이용한 강제 업데이트 로직 추가
2023-07-25 02:40:41 +09:00
edbaceba0b 회원가입 후 초기 알림설정 기능 추가 2023-07-24 14:54:15 +09:00
6c8183b12f 이용약관 보기 - 패키지 이동 2023-07-24 06:17:32 +09:00
d60eb7e408 메인 - 하단 탭 추가 2023-07-24 06:15:06 +09:00
41c6228af5 스플래시 - UI 적용 2023-07-24 05:50:46 +09:00
d562e9199c 회원가입, 로그인 페이지 추가 2023-07-24 05:38:49 +09:00
c1054c5ede KOIN 설정 적용 2023-07-23 19:50:34 +09:00
819 changed files with 56263 additions and 112 deletions

7
.gitignore vendored
View File

@@ -20,7 +20,8 @@ bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
release/
/release/
/debug/
# Gradle files
.gradle/
@@ -302,7 +303,7 @@ fabric.properties
### AndroidStudio Patch ###
!/gradle/wrapper/gradle-wrapper.jar
app/debug/*
app/release/*
app/debug/
app/release/
# End of https://www.toptal.com/developers/gitignore/api/macos,android,androidstudio,visualstudiocode,git,kotlin,java

View File

@@ -1,6 +1,7 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.gms.google-services'
id 'com.google.android.gms.oss-licenses-plugin'
id 'kotlin-kapt'
@@ -8,6 +9,7 @@ plugins {
id 'org.jlleitschuh.gradle.ktlint'
id 'io.objectbox'
id 'com.google.firebase.crashlytics'
}
android {
@@ -18,10 +20,13 @@ android {
enabled true
}
buildFeatures.dataBinding = true
buildFeatures {
dataBinding true
}
lintOptions {
checkDependencies true
checkReleaseBuilds false
}
dependenciesInfo {
@@ -35,21 +40,30 @@ android {
applicationId "kr.co.vividnext.sodalive"
minSdk 23
targetSdk 33
versionCode 1
versionName "1.0.0"
versionCode 10
versionName "1.1.0"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
buildConfigField 'String', 'BASE_URL', '"https://api.sodalive.net"'
buildConfigField 'String', 'BOOTPAY_APP_ID', '"64c35be1d25985001dc50c87"'
buildConfigField 'String', 'AGORA_APP_ID', '"e34e40046e9847baba3adfe2b8ffb4f6"'
buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"15cadeea4ba94ff7b091c9a10f4bf4a6"'
}
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
applicationIdSuffix '.debug'
buildConfigField 'String', 'BASE_URL', '"https://test-api.sodalive.net"'
buildConfigField 'String', 'BOOTPAY_APP_ID', '"6242a7772701800023f68b2e"'
buildConfigField 'String', 'AGORA_APP_ID', '"b96574e191a9430fa54c605528aa3ef7"'
buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"ae18ade3afcf4086bd4397726eb0654c"'
}
}
compileOptions {
@@ -85,13 +99,13 @@ dependencies {
implementation "io.insert-koin:koin-android:3.1.3"
// Preference
implementation("androidx.preference:preference-ktx:1.2.0") {
implementation("androidx.preference:preference-ktx:1.2.1") {
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
}
// Gson
implementation "com.google.code.gson:gson:2.9.0"
implementation "com.google.code.gson:gson:2.10.1"
// Network
implementation "com.squareup.retrofit2:retrofit:2.9.0"
@@ -100,12 +114,42 @@ dependencies {
implementation "com.squareup.okhttp3:logging-interceptor:4.9.3"
// RxJava3
implementation "io.reactivex.rxjava3:rxjava:3.1.3"
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
implementation "io.reactivex.rxjava3:rxjava:3.1.6"
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// permission
implementation "io.github.ParkSangGwon:tedpermission-normal:3.3.0"
implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.github.zhpanvip:bannerviewpager:3.5.7'
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.1'
// Firebase
implementation platform('com.google.firebase:firebase-bom:32.2.2')
implementation 'com.google.firebase:firebase-dynamic-links-ktx'
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
implementation 'com.google.firebase:firebase-messaging-ktx'
implementation 'com.google.firebase:firebase-config-ktx'
// bootpay
implementation "io.github.bootpay:android:4.3.4"
// agora
implementation "io.agora.rtc:voice-sdk:4.1.0-1"
implementation 'io.agora.rtm:rtm-sdk:1.5.3'
// sound visualizer
implementation "com.gauravk.audiovisualizer:audiovisualizer:0.9.2"
// Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
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

@@ -0,0 +1,67 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:2209417227252155460",
"lastPropertyId": "8:7803281435927194929",
"name": "PlaybackTracking",
"properties": [
{
"id": "1:3889922602505997244",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:874896374244616380",
"name": "contentId",
"type": 6
},
{
"id": "3:305496269372931228",
"name": "totalDuration",
"type": 5
},
{
"id": "4:1202262957765031780",
"name": "startPosition",
"type": 5
},
{
"id": "5:1595250877919247629",
"name": "isFree",
"type": 1
},
{
"id": "6:4066577743967565922",
"name": "isPreview",
"type": 1
},
{
"id": "7:7482414752180672089",
"name": "endPosition",
"type": 5
},
{
"id": "8:7803281435927194929",
"name": "playDateTime",
"type": 9
}
],
"relations": []
}
],
"lastEntityId": "1:2209417227252155460",
"lastIndexId": "0:0",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [],
"retiredRelationUids": [],
"version": 1
}

126
app/proguard-rules.pro vendored
View File

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

View File

@@ -0,0 +1,39 @@
{
"project_info": {
"project_number": "758414412471",
"project_id": "sodalive-test",
"storage_bucket": "sodalive-test.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:758414412471:android:dcea9dff87fa125c7a5b32",
"android_client_info": {
"package_name": "kr.co.vividnext.sodalive.debug"
}
},
"oauth_client": [
{
"client_id": "758414412471-g35socquiplhaamhfl4e6bsta5blabi7.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAeNDVDY_r5afz97L1NPvQC6oFy5lPXHNI"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "758414412471-g35socquiplhaamhfl4e6bsta5blabi7.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@@ -2,7 +2,49 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<application
android:name=".app.SodaLiveApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@@ -10,7 +52,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/Theme.SodaLive"
android:usesCleartextTraffic="true"
@@ -23,8 +65,65 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="sodalive.page.link" />
<data android:host="sodalive.net" />
</intent-filter>
</activity>
<activity android:name=".main.MainActivity" />
<activity android:name=".user.login.LoginActivity" />
<activity android:name=".user.signup.SignUpActivity" />
<activity android:name=".settings.terms.TermsActivity" />
<activity android:name=".user.find_password.FindPasswordActivity" />
<activity android:name=".mypage.can.status.CanStatusActivity" />
<activity android:name=".mypage.can.charge.CanChargeActivity" />
<activity android:name=".mypage.can.payment.CanPaymentActivity" />
<activity android:name=".live.room.create.LiveRoomCreateActivity" />
<activity android:name=".live.room.update.LiveRoomEditActivity" />
<activity android:name=".live.reservation.complete.LiveReservationCompleteActivity" />
<activity
android:name=".live.room.LiveRoomActivity"
android:windowSoftInputMode="stateAlwaysHidden|adjustPan" />
<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=".message.text.TextMessageWriteActivity" />
<activity android:name=".message.text.TextMessageDetailActivity" />
<activity android:name=".message.SelectMessageRecipientActivity" />
<activity android:name=".settings.SettingsActivity" />
<activity android:name=".settings.signout.SignOutActivity" />
<activity android:name=".settings.notice.NoticeActivity" />
<activity android:name=".settings.notice.NoticeDetailActivity" />
<activity android:name=".settings.event.EventActivity" />
<activity android:name=".settings.event.EventDetailActivity" />
<activity android:name=".settings.notification.NotificationSettingsActivity" />
<activity android:name=".live.reservation_status.LiveReservationStatusActivity" />
<activity android:name=".live.reservation_status.LiveReservationCancelActivity" />
<activity android:name=".audio_content.AudioContentActivity" />
<activity android:name=".audio_content.detail.AudioContentDetailActivity" />
<activity android:name=".audio_content.modify.AudioContentModifyActivity" />
<activity android:name=".audio_content.order.AudioContentOrderListActivity" />
<activity android:name=".audio_content.upload.AudioContentUploadActivity" />
<activity android:name=".following.FollowingCreatorActivity" />
<activity android:name=".live.now.all.LiveNowAllActivity" />
<activity android:name=".live.reservation.all.LiveReservationAllActivity" />
<activity android:name=".mypage.service_center.ServiceCenterActivity" />
<activity android:name=".onboarding.OnBoardingActivity" />
<activity android:name=".mypage.profile.ProfileUpdateActivity" />
<activity android:name=".mypage.profile.nickname.NicknameUpdateActivity" />
<activity android:name=".mypage.profile.password.ModifyPasswordActivity" />
<activity android:name=".audio_content.curation.AudioContentCurationActivity" />
<activity android:name=".audio_content.all.AudioContentNewAllActivity" />
<activity android:name=".audio_content.all.AudioContentRankingAllActivity" />
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
@@ -32,6 +131,27 @@
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
android:theme="@style/Theme.AppCompat.DayNight" />
</application>
<service
android:name=".common.SodaLiveService"
android:stopWithTask="false" />
<service android:name=".audio_content.AudioContentPlayService" />
<!-- [START firebase_service] -->
<service
android:name=".fcm.SodaFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- [END firebase_service] -->
<!-- [START fcm_default_channel] -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />
<!-- [END fcm_default_channel] -->
</application>
</manifest>

View File

@@ -0,0 +1,205 @@
package kr.co.vividnext.sodalive.agora
import android.content.Context
import com.orhanobut.logger.Logger
import io.agora.rtc2.Constants
import io.agora.rtc2.IRtcEngineEventHandler
import io.agora.rtc2.RtcEngine
import io.agora.rtm.ErrorInfo
import io.agora.rtm.ResultCallback
import io.agora.rtm.RtmChannel
import io.agora.rtm.RtmChannelListener
import io.agora.rtm.RtmClient
import io.agora.rtm.RtmClientListener
import io.agora.rtm.SendMessageOptions
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.live.room.LiveRoomRequestType
import kotlin.concurrent.thread
class Agora(
private val context: Context,
private val rtcEventHandler: IRtcEngineEventHandler,
private val rtmClientListener: RtmClientListener
) {
// RTM client instance
private var rtmClient: RtmClient? = null
// RTM channel instance
private var rtmChannel: RtmChannel? = null
private var rtcEngine: RtcEngine? = null
init {
initAgoraEngine()
}
private fun initAgoraEngine() {
try {
rtcEngine = RtcEngine.create(
context,
BuildConfig.AGORA_APP_ID,
rtcEventHandler
)
rtcEngine!!.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
rtcEngine!!.setAudioProfile(
Constants.AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO,
Constants.AUDIO_SCENARIO_GAME_STREAMING
)
rtcEngine!!.enableAudio()
rtcEngine!!.enableAudioVolumeIndication(500, 3, true)
rtmClient = RtmClient.createInstance(
context,
BuildConfig.AGORA_APP_ID,
rtmClientListener
)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun deInitAgoraEngine() {
if (rtcEngine != null) {
rtcEngine!!.leaveChannel()
thread {
RtcEngine.destroy()
rtcEngine = null
}
}
rtmChannel?.leave(null)
rtmChannel?.release()
rtmClient?.logout(null)
}
fun inputChat(message: String) {
val rtmMessage = rtmClient!!.createMessage()
rtmMessage.text = message
rtmChannel!!.sendMessage(
rtmMessage,
object : ResultCallback<Void?> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
}
override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorDescription}")
}
}
)
}
fun joinRtcChannel(uid: Int, rtcToken: String, channelName: String) {
rtcEngine!!.joinChannel(
rtcToken,
channelName,
"",
uid
)
}
fun createRtmChannelAndLogin(
uid: String,
rtmToken: String,
channelName: String,
rtmChannelListener: RtmChannelListener,
rtmChannelJoinSuccess: () -> Unit,
rtmChannelJoinFail: () -> Unit
) {
rtmChannel = rtmClient!!.createChannel(channelName, rtmChannelListener)
rtmClient!!.login(
rtmToken,
uid,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
rtmChannel!!.join(object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("rtmChannel join - onSuccess")
rtmChannelJoinSuccess()
}
override fun onFailure(p0: ErrorInfo?) {
rtmChannelJoinFail()
}
})
}
override fun onFailure(p0: ErrorInfo?) {
}
}
)
}
fun sendRawMessageToGroup(
rawMessage: ByteArray,
onSuccess: (() -> Unit)? = null,
onFailure: (() -> Unit)? = null
) {
val message = rtmClient!!.createMessage()
message.rawMessage = rawMessage
rtmChannel!!.sendMessage(
message,
object : ResultCallback<Void?> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
onSuccess?.invoke()
}
override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorDescription}")
onFailure?.invoke()
}
}
)
}
fun setClientRole(role: Int) {
rtcEngine!!.setClientRole(role)
}
fun muteLocalAudioStream(muted: Boolean) {
rtcEngine?.muteLocalAudioStream(muted)
}
fun muteAllRemoteAudioStreams(mute: Boolean) {
rtcEngine?.muteAllRemoteAudioStreams(mute)
}
fun sendRawMessageToPeer(
receiverUid: String,
requestType: LiveRoomRequestType,
onSuccess: () -> Unit
) {
val option = SendMessageOptions()
val message = rtmClient!!.createMessage()
message.rawMessage = requestType.toString().toByteArray()
rtmClient!!.sendMessageToPeer(
receiverUid,
message,
option,
object : ResultCallback<Void?> {
override fun onSuccess(aVoid: Void?) {
onSuccess()
}
override fun onFailure(errorInfo: ErrorInfo) {
}
}
)
}
fun rtmChannelIsNull(): Boolean {
return rtmChannel == null
}
fun getConnectionState(): Int {
return rtcEngine!!.connectionState
}
}

View File

@@ -0,0 +1,50 @@
package kr.co.vividnext.sodalive.app
import android.app.Application
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Build
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.SharedPreferenceManager
import kr.co.vividnext.sodalive.di.AppDI
class SodaLiveApp : Application() {
override fun onCreate() {
super.onCreate()
Logger.addLogAdapter(object : AndroidLogAdapter() {
override fun isLoggable(priority: Int, tag: String?): Boolean {
return BuildConfig.DEBUG && isDebuggable()
}
})
AppDI(applicationContext, BuildConfig.DEBUG && isDebuggable())
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
SharedPreferenceManager.init(applicationContext)
}
private fun isDebuggable(): Boolean {
var debuggable = false
try {
val appInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getApplicationInfo(
packageName,
PackageManager.ApplicationInfoFlags.of(0L)
)
} else {
packageManager.getApplicationInfo(packageName, 0)
}
debuggable = 0 != appInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
} catch (e: PackageManager.NameNotFoundException) {
/* debuggable variable will remain false */
}
return debuggable
}
}

View File

@@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.audio_content
import com.google.gson.annotations.SerializedName
import java.util.TimeZone
data class AddAllPlaybackTrackingRequest(
@SerializedName("timezone") val timezone: String = TimeZone.getDefault().id,
@SerializedName("trackingDataList") val trackingDataList: List<PlaybackTrackingData>
)
data class PlaybackTrackingData(
@SerializedName("contentId") val contentId: Long,
@SerializedName("playDateTime") val playDateTime: String,
@SerializedName("isPreview") val isPreview: Boolean,
)

View File

@@ -0,0 +1,212 @@
package kr.co.vividnext.sodalive.audio_content
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
ActivityAudioContentBinding::inflate
) {
private val viewModel: AudioContentViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var audioContentAdapter: AudioContentAdapter
private var userId: Long = 0
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
activityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
viewModel.page = 1
viewModel.getAudioContentList(userId = userId) { finish() }
}
}
super.onCreate(savedInstanceState)
if (userId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
bindData()
viewModel.getAudioContentList(userId = userId) { finish() }
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "콘텐츠 전체보기"
binding.toolbar.tvBack.setOnClickListener { finish() }
audioContentAdapter = AudioContentAdapter {
val intent = Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
activityResultLauncher.launch(intent)
}
binding.rvAudioContent.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
binding.rvAudioContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
audioContentAdapter.itemCount - 1 -> {
outRect.bottom = 0
}
else -> {
outRect.bottom = 13.3f.dpToPx().toInt()
}
}
}
})
binding.rvAudioContent.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.getAudioContentList(userId = userId) { }
}
}
})
binding.rvAudioContent.adapter = audioContentAdapter
binding.tvSortNewest.setOnClickListener {
viewModel.changeSort(AudioContentViewModel.Sort.NEWEST)
}
binding.tvSortPriceLow.setOnClickListener {
viewModel.changeSort(AudioContentViewModel.Sort.PRICE_LOW)
}
binding.tvSortPriceHigh.setOnClickListener {
viewModel.changeSort(AudioContentViewModel.Sort.PRICE_HIGH)
}
if (userId == SharedPreferenceManager.userId) {
binding.tvNewContent.visibility = View.VISIBLE
binding.tvNewContent.setOnClickListener {
startActivity(
Intent(
applicationContext,
AudioContentUploadActivity::class.java
)
)
}
} else {
binding.tvNewContent.visibility = View.GONE
}
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
}
}
viewModel.audioContentListLiveData.observe(this) {
if (viewModel.page - 1 == 1) {
audioContentAdapter.items.clear()
binding.rvAudioContent.scrollToPosition(0)
}
binding.tvTotalCount.text = "${it.totalCount}"
audioContentAdapter.items.addAll(it.items)
audioContentAdapter.notifyDataSetChanged()
}
viewModel.sort.observe(this) {
deselectSort()
selectSort(
when (it) {
AudioContentViewModel.Sort.PRICE_HIGH -> {
binding.tvSortPriceHigh
}
AudioContentViewModel.Sort.PRICE_LOW -> {
binding.tvSortPriceLow
}
else -> {
binding.tvSortNewest
}
}
)
viewModel.getAudioContentList(userId = userId) { finish() }
}
}
private fun deselectSort() {
val color = ContextCompat.getColor(
applicationContext,
R.color.color_88e2e2e2
)
binding.tvSortNewest.setTextColor(color)
binding.tvSortPriceLow.setTextColor(color)
binding.tvSortPriceHigh.setTextColor(color)
}
private fun selectSort(view: TextView) {
view.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_e2e2e2
)
)
}
}

View File

@@ -0,0 +1,66 @@
package kr.co.vividnext.sodalive.audio_content
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioContentBinding
import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListItem
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentAdapter.ViewHolder>() {
val items = mutableListOf<GetAudioContentListItem>()
inner class ViewHolder(
private val binding: ItemAudioContentBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentListItem) {
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
binding.tvTitle.text = item.title
binding.tvTheme.text = item.themeStr
binding.tvDuration.text = item.duration
binding.tvLikeCount.text = item.likeCount.moneyFormat()
binding.tvCommentCount.text = item.commentCount.moneyFormat()
if (item.price < 1) {
binding.tvPrice.text = "무료"
binding.tvPrice.setCompoundDrawables(null, null, null, null)
} else {
binding.tvPrice.text = item.price.moneyFormat()
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_can,
0,
0,
0
)
}
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAudioContentBinding.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

@@ -0,0 +1,185 @@
package kr.co.vividnext.sodalive.audio_content
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audio_content.all.GetNewContentAllResponse
import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListResponse
import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest
import kr.co.vividnext.sodalive.audio_content.comment.RegisterAudioContentCommentRequest
import kr.co.vividnext.sodalive.audio_content.curation.GetCurationContentResponse
import kr.co.vividnext.sodalive.audio_content.detail.GetAudioContentDetailResponse
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeResponse
import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationRequest
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRanking
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListResponse
import kr.co.vividnext.sodalive.audio_content.order.OrderRequest
import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.DELETE
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 AudioContentApi {
@GET("/audio-content")
fun getAudioContentList(
@Query("creator-id") id: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort-type") sort: AudioContentViewModel.Sort,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentListResponse>>
@GET("/audio-content/theme")
fun getAudioContentThemeList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentThemeResponse>>>
@POST("/audio-content")
@Multipart
fun uploadAudioContent(
@Part coverImage: MultipartBody.Part,
@Part contentFile: MultipartBody.Part,
@Part("request") request: RequestBody,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/{id}")
fun getAudioContentDetail(
@Path("id") id: Long,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentDetailResponse>>
@POST("/order/audio-content")
fun orderAudioContent(
@Body request: OrderRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/order/audio-content")
fun getAudioContentOrderList(
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentOrderListResponse>>
@POST("/audio-content/playback-tracking")
fun addAllPlaybackTracking(
@Body request: AddAllPlaybackTrackingRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/audio-content/comment")
fun registerComment(
@Body request: RegisterAudioContentCommentRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/{id}/comment")
fun getAudioContentCommentList(
@Path("id") id: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentCommentListResponse>>
@GET("/audio-content/comment/{id}")
fun getAudioContentCommentCommentList(
@Path("id") id: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentCommentListResponse>>
@PUT("/audio-content/like")
fun likeContent(
@Body request: PutAudioContentLikeRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<PutAudioContentLikeResponse>>
@PUT("/audio-content")
@Multipart
fun modifyAudioContent(
@Part coverImage: MultipartBody.Part?,
@Part("request") request: RequestBody,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@DELETE("/audio-content/{id}")
fun deleteAudioContent(
@Path("id") id: Long,
@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,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@GET("/audio-content/main/new/all")
fun getNewContentAllOfTheme(
@Query("theme") theme: String,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetNewContentAllResponse>>
@POST("/audio-content/donation")
fun donation(
@Body request: AudioContentDonationRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@PUT("/audio-content/comment")
fun modifyComment(
@Body request: ModifyCommentRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/curation/{id}")
fun getAudioContentListByCurationId(
@Path("id") id: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort-type") sort: AudioContentViewModel.Sort,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetCurationContentResponse>>
@GET("/audio-content/main/theme")
fun getNewContentThemeList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<String>>>
@GET("/audio-content/ranking-sort-type")
fun getContentRankingSortType(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<String>>>
@GET("/audio-content/ranking")
fun getContentRanking(
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort-type") sortType: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentRanking>>
}

View File

@@ -0,0 +1,561 @@
package kr.co.vividnext.sodalive.audio_content
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import androidx.core.app.NotificationCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.main.MainActivity
import org.koin.android.ext.android.inject
class AudioContentPlayService :
Service(),
MediaPlayer.OnPreparedListener,
MediaPlayer.OnCompletionListener {
private var playbackTrackingId: Long = 0
private val playbackTrackingRepository: PlaybackTrackingRepository by inject()
private lateinit var mediaPlayer: MediaPlayer
private var url: String? = null
private var title: String? = null
private var isFree: Boolean? = null
private var isPreview: Boolean? = null
private var nickname: String? = null
private var contentId: Long? = null
private var creatorId: Long? = null
private var coverImageUrl: String? = null
private var isPlaying = false
private val handler = Handler(Looper.getMainLooper())
private var changeMediaPlayerPositionRunnable = object : Runnable {
override fun run() {
val intent = Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, mediaPlayer.currentPosition)
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
sendBroadcast(intent)
handler.postDelayed(this, 1000)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
MusicAction.INIT.name -> {
val contentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
if (this.contentId != null && this.contentId == contentId) {
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI,
true
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
isPlaying
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_DURATION,
mediaPlayer.duration
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PROGRESS,
mediaPlayer.currentPosition
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
contentId
)
}
)
}
if (
this.contentId != null &&
title != null &&
nickname != null &&
coverImageUrl != null
) {
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
true
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
isPlaying
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_TITLE,
title
)
putExtra(
Constants.EXTRA_NICKNAME,
nickname
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL,
coverImageUrl
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
this@AudioContentPlayService.contentId
)
}
)
} else {
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
false
)
}
)
}
}
MusicAction.PLAY.name -> {
if (!isPlaying && this::mediaPlayer.isInitialized) {
mediaPlayer.start()
toggleIsPlaying()
updateNotification()
}
}
MusicAction.PAUSE.name -> {
if (isPlaying && this::mediaPlayer.isInitialized) {
mediaPlayer.pause()
toggleIsPlaying()
updateNotification()
}
}
MusicAction.STOP.name -> {
if (this::mediaPlayer.isInitialized) {
mediaPlayer.stop()
setEndPositionPlaybackTracking(mediaPlayer.currentPosition)
toggleIsPlaying(false)
onStopService()
}
}
MusicAction.CONDITIONAL_STOP.name -> {
val contentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
if (
this.contentId != null &&
this.contentId == contentId &&
this::mediaPlayer.isInitialized
) {
mediaPlayer.stop()
setEndPositionPlaybackTracking(mediaPlayer.currentPosition)
toggleIsPlaying(false)
onStopService()
}
}
MusicAction.PROGRESS.name -> {
val progress = intent.getIntExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, 0)
if (progress > 0 && this::mediaPlayer.isInitialized) {
if (contentId != null) {
saveNewPlaybackTracking(
totalDuration = mediaPlayer.duration,
progress = progress
)
}
mediaPlayer.seekTo(progress)
}
}
else -> {
val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
if (contentId != null && this.contentId == contentId) {
if (isPlaying) {
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PAUSE
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
contentId
)
}
)
} else {
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PLAY
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
contentId
)
}
)
}
} else {
url = intent?.getStringExtra(Constants.EXTRA_AUDIO_CONTENT_URL)
title = intent?.getStringExtra(Constants.EXTRA_AUDIO_CONTENT_TITLE)
isFree = intent?.getBooleanExtra(
Constants.EXTRA_AUDIO_CONTENT_FREE,
true
)
isPreview = intent?.getBooleanExtra(
Constants.EXTRA_AUDIO_CONTENT_PREVIEW,
true
)
nickname = intent?.getStringExtra(Constants.EXTRA_NICKNAME)
coverImageUrl = intent?.getStringExtra(
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL
)
creatorId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, 0)
this.contentId = contentId
if (url != null) {
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_LOADING,
true
)
}
)
if (isPlaying && this::mediaPlayer.isInitialized) {
mediaPlayer.stop()
setEndPositionPlaybackTracking(mediaPlayer.currentPosition)
mediaPlayer.release()
toggleIsPlaying()
}
initMediaPlayer()
mediaPlayer.setDataSource(url)
mediaPlayer.prepareAsync()
}
}
}
}
return START_NOT_STICKY
}
override fun onDestroy() {
if (this::mediaPlayer.isInitialized) {
mediaPlayer.release()
}
onStopService()
super.onDestroy()
}
override fun onBind(p0: Intent?): IBinder? {
return null
}
override fun onCompletion(mp: MediaPlayer) {
setEndPositionPlaybackTracking(mp.currentPosition)
if (SharedPreferenceManager.isContentPlayLoop) {
saveNewPlaybackTracking(totalDuration = mp.duration, progress = 0)
mp.start()
} else {
toggleIsPlaying(false)
mediaPlayer.release()
onStopService()
}
}
private fun toggleIsPlaying(isPlaying: Boolean? = null) {
this.isPlaying = isPlaying ?: !this.isPlaying
if (this.isPlaying) {
handler.postDelayed(changeMediaPlayerPositionRunnable, 1000)
} else {
handler.removeCallbacks(changeMediaPlayerPositionRunnable)
}
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI,
true
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
this@AudioContentPlayService.isPlaying
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
contentId
)
}
)
if (isPlaying != null && !isPlaying) {
resetAudioData()
}
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
this@AudioContentPlayService.isPlaying
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
contentId != null
)
}
)
}
private fun resetAudioData() {
url = null
title = null
nickname = null
contentId = null
}
private fun initMediaPlayer() {
mediaPlayer = MediaPlayer()
mediaPlayer.setOnPreparedListener(this)
mediaPlayer.setOnCompletionListener(this)
mediaPlayer.setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
}
override fun onPrepared(mp: MediaPlayer?) {
if (this::mediaPlayer.isInitialized) {
saveNewPlaybackTracking(totalDuration = mediaPlayer.duration, progress = 0)
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PLAY
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_DURATION,
mediaPlayer.duration
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
contentId
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ALERT_PREVIEW,
true
)
}
)
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
false
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
true
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_TITLE,
title
)
putExtra(
Constants.EXTRA_NICKNAME,
nickname
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL,
coverImageUrl
)
}
)
}
}
private fun updateNotification() {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
)
val channelId = "audio_content_play_channel"
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"콘텐츠 알림 채널",
NotificationManager.IMPORTANCE_LOW
)
notificationManager.createNotificationChannel(channel)
}
val playPauseIcon =
if (isPlaying) R.drawable.ic_noti_pause else R.drawable.ic_noti_play
val playPauseAction =
if (isPlaying) MusicAction.PAUSE.name else MusicAction.PLAY.name
Glide
.with(this)
.asBitmap()
.load(coverImageUrl)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
val notificationBuilder = NotificationCompat
.Builder(this@AudioContentPlayService, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(resource)
.setContentTitle(title ?: "오디오 콘텐츠")
.setContentText(nickname ?: "")
.setContentIntent(pendingIntent)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.addAction(
NotificationCompat
.Action
.Builder(
playPauseIcon,
"Play_or_Pause",
getServiceIntent(playPauseAction)
).build()
)
.addAction(
NotificationCompat
.Action
.Builder(
R.drawable.ic_noti_stop,
"Stop",
getServiceIntent(MusicAction.STOP.name)
).build()
)
notificationBuilder.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setShowActionsInCompactView(0, 1)
)
startForeground(1, notificationBuilder.build())
}
override fun onLoadCleared(placeholder: Drawable?) {
}
})
}
private fun getServiceIntent(action: String): PendingIntent {
val intent = Intent(this, AudioContentPlayService::class.java)
intent.action = action
return PendingIntent.getService(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
)
}
private fun saveNewPlaybackTracking(totalDuration: Int, progress: Int) {
if (creatorId != SharedPreferenceManager.userId) {
playbackTrackingId = playbackTrackingRepository.savePlaybackTracking(
PlaybackTracking(
contentId = contentId!!,
totalDuration = totalDuration,
startPosition = progress,
isFree = isFree!!,
isPreview = isPreview!!
)
)
}
}
private fun setEndPositionPlaybackTracking(progress: Int) {
if (creatorId != SharedPreferenceManager.userId && playbackTrackingId > 0) {
val playbackTracking = playbackTrackingRepository
.getPlaybackTracking(playbackTrackingId)
if (playbackTracking != null) {
playbackTracking.endPosition = progress
playbackTrackingRepository.savePlaybackTracking(playbackTracking)
}
playbackTrackingId = 0
}
}
private fun onStopService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
stopForeground(true)
}
stopSelf()
}
enum class MusicAction {
PLAY, PAUSE, STOP, PROGRESS, INIT, CONDITIONAL_STOP
}
}

View File

@@ -0,0 +1,180 @@
package kr.co.vividnext.sodalive.audio_content
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest
import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationRequest
import kr.co.vividnext.sodalive.audio_content.order.OrderRequest
import kr.co.vividnext.sodalive.audio_content.order.OrderType
import kr.co.vividnext.sodalive.user.CreatorFollowRequestRequest
import kr.co.vividnext.sodalive.user.UserApi
import okhttp3.MultipartBody
import okhttp3.RequestBody
import java.util.TimeZone
class AudioContentRepository(
private val api: AudioContentApi,
private val userApi: UserApi
) {
fun getAudioContentListByCurationId(
curationId: Long,
page: Int,
size: Int,
sort: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
token: String
) = api.getAudioContentListByCurationId(
id = curationId,
page = page - 1,
size = size,
sort = sort,
authHeader = token
)
fun getAudioContentList(
id: Long,
page: Int,
size: Int,
sort: AudioContentViewModel.Sort,
token: String
) = api.getAudioContentList(
id = id,
page = page - 1,
size = size,
sort = sort,
authHeader = token
)
fun getAudioContentThemeList(token: String) = api.getAudioContentThemeList(token)
fun uploadAudioContent(
coverImage: MultipartBody.Part,
contentFile: MultipartBody.Part,
request: RequestBody,
token: String
) = api.uploadAudioContent(
coverImage = coverImage,
contentFile = contentFile,
request = request,
authHeader = token
)
fun modifyAudioContent(
coverImage: MultipartBody.Part? = null,
request: RequestBody,
token: String
) = api.modifyAudioContent(
coverImage = coverImage,
request = request,
authHeader = token
)
fun deleteAudioContent(
id: Long,
token: String
) = api.deleteAudioContent(
id = id,
authHeader = token
)
fun getAudioContentDetail(audioContentId: Long, token: String) = api.getAudioContentDetail(
id = audioContentId,
timezone = TimeZone.getDefault().id,
authHeader = token
)
fun registerNotification(
creatorId: Long,
token: String
) = userApi.creatorFollow(
request = CreatorFollowRequestRequest(creatorId = creatorId),
authHeader = token
)
fun unRegisterNotification(
creatorId: Long,
token: String
) = userApi.creatorUnFollow(
request = CreatorFollowRequestRequest(creatorId = creatorId),
authHeader = token
)
fun orderContent(
contentId: Long,
orderType: OrderType,
token: String
) = api.orderAudioContent(
request = OrderRequest(
contentId = contentId,
orderType = orderType,
container = "aos"
),
authHeader = token
)
fun getAudioContentOrderList(
page: Int,
size: Int,
token: String
) = api.getAudioContentOrderList(
page = page - 1,
size = size,
authHeader = token
)
fun addAllPlaybackTracking(
request: AddAllPlaybackTrackingRequest,
token: String
) = api.addAllPlaybackTracking(request, authHeader = token)
fun likeContent(
request: PutAudioContentLikeRequest,
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
)
fun getNewContentAllOfTheme(
theme: String,
page: Int,
size: Int,
token: String
) = api.getNewContentAllOfTheme(
theme = theme,
page = page - 1,
size = size,
authHeader = token
)
fun getNewContentThemeList(token: String) = api.getNewContentThemeList(authHeader = token)
fun donation(
contentId: Long,
can: Int,
comment: String,
token: String
) = api.donation(
request = AudioContentDonationRequest(
contentId = contentId,
donationCan = can,
comment = comment
),
authHeader = token
)
fun getContentRankingSortType(token: String) = api.getContentRankingSortType(authHeader = token)
fun getContentRanking(
page: Int,
size: Int,
sortType: String = "매출",
token: String
) = api.getContentRanking(
page = page - 1,
size = size,
sortType = sortType,
authHeader = token
)
}

View File

@@ -0,0 +1,102 @@
package kr.co.vividnext.sodalive.audio_content
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.annotations.SerializedName
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.GetAudioContentListResponse
class AudioContentViewModel(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 _audioContentListLiveData = MutableLiveData<GetAudioContentListResponse>()
val audioContentListLiveData: LiveData<GetAudioContentListResponse>
get() = _audioContentListLiveData
private val _sort = MutableLiveData(Sort.NEWEST)
val sort: LiveData<Sort>
get() = _sort
enum class Sort {
@SerializedName("NEWEST")
NEWEST,
@SerializedName("PRICE_HIGH")
PRICE_HIGH,
@SerializedName("PRICE_LOW")
PRICE_LOW
}
private var isLast = false
var page = 1
private val size = 10
fun getAudioContentList(userId: Long, onFailure: (() -> Unit)? = null) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentList(
id = userId,
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}",
sort = _sort.value!!
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
page += 1
_audioContentListLiveData.postValue(it.data!!)
} else {
isLast = true
}
} 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 changeSort(sort: Sort) {
page = 1
isLast = false
_sort.postValue(sort)
}
}

View File

@@ -0,0 +1,23 @@
package kr.co.vividnext.sodalive.audio_content
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Entity
data class PlaybackTracking(
@Id
var id: Long = 0,
var contentId: Long,
var totalDuration: Int,
var startPosition: Int,
var isFree: Boolean,
var isPreview: Boolean,
var endPosition: Int? = null,
var playDateTime: String = SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss",
Locale.getDefault()
).format(Date())
)

View File

@@ -0,0 +1,29 @@
package kr.co.vividnext.sodalive.audio_content
import kr.co.vividnext.sodalive.common.ObjectBox
class PlaybackTrackingRepository(private val objectBox: ObjectBox) {
fun savePlaybackTracking(data: PlaybackTracking): Long {
return objectBox.playbackTrackingBox.put(data)
}
fun getPlaybackTracking(id: Long): PlaybackTracking? {
val query = objectBox.playbackTrackingBox
.query(PlaybackTracking_.id.equal(id))
.build()
val playbackTracking = query.findFirst()
query.close()
return playbackTracking
}
fun getAllPlaybackTracking(): List<PlaybackTracking> {
return objectBox
.playbackTrackingBox
.all
}
fun removeAllPlaybackTracking() {
objectBox.playbackTrackingBox.removeAll()
}
}

View File

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

View File

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

View File

@@ -0,0 +1,127 @@
package kr.co.vividnext.sodalive.audio_content.all
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentNewAllViewModel(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _themeListLiveData = MutableLiveData<List<String>>()
val themeListLiveData: LiveData<List<String>>
get() = _themeListLiveData
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _newContentListLiveData
private var _newContentTotalCountLiveData = MutableLiveData<Int>()
val newContentTotalCountLiveData: LiveData<Int>
get() = _newContentTotalCountLiveData
private var isLast = false
private var page = 1
private val size = 10
private var selectedTheme = ""
fun getNewContentList() {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getNewContentAllOfTheme(
theme = if (selectedTheme == "전체") {
""
} else {
selectedTheme
},
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
page += 1
_newContentListLiveData.postValue(it.data.items)
_newContentTotalCountLiveData.postValue(it.data.totalCount)
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
fun getThemeList() {
compositeDisposable.add(
repository.getNewContentThemeList(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val themeList = listOf("전체").union(it.data).toList()
_themeListLiveData.postValue(themeList)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun selectTheme(theme: String) {
isLast = false
page = 1
selectedTheme = theme
getNewContentList()
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,174 @@
package kr.co.vividnext.sodalive.audio_content.comment
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentCommentAdapter(
private val creatorId: Long,
private val modifyComment: (Long, String) -> Unit,
private val onClickDelete: (Long) -> Unit,
private val onItemClick: (GetAudioContentCommentListItem) -> Unit
) : RecyclerView.Adapter<AudioContentCommentAdapter.ViewHolder>() {
var items = mutableSetOf<GetAudioContentCommentListItem>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentCommentBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentCommentListItem) {
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
val tvCommentLayoutParams = binding.tvComment.layoutParams as LinearLayout.LayoutParams
val can = item.donationCan
if (can > 0) {
tvCommentLayoutParams.topMargin = 0
binding.llDonationCan.visibility = View.VISIBLE
binding.tvDonationCan.text = can.moneyFormat()
binding.llDonationCan.setBackgroundResource(
when {
can >= 100000 -> {
R.drawable.bg_round_corner_10_7_973a3a
}
can >= 50000 -> {
R.drawable.bg_round_corner_10_7_d85e37
}
can >= 10000 -> {
R.drawable.bg_round_corner_10_7_d38c38
}
can >= 5000 -> {
R.drawable.bg_round_corner_10_7_59548f
}
can >= 1000 -> {
R.drawable.bg_round_corner_10_7_4d6aa4
}
can >= 500 -> {
R.drawable.bg_round_corner_10_7_2d7390
}
else -> {
R.drawable.bg_round_corner_10_7_548f7d
}
}
)
} else {
tvCommentLayoutParams.topMargin = 13.3f.dpToPx().toInt()
binding.llDonationCan.visibility = View.GONE
}
binding.tvComment.layoutParams = tvCommentLayoutParams
binding.tvComment.text = item.comment
binding.tvCommentDate.text = item.date
binding.tvCommentNickname.text = item.nickname
binding.tvWriteReply.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,
ItemAudioContentCommentBinding.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,77 @@
package kr.co.vividnext.sodalive.audio_content.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
class AudioContentCommentFragment(
private val creatorId: Long,
private val audioContentId: 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 = AudioContentCommentListFragment.newInstance(
creatorId = creatorId,
audioContentId = audioContentId
)
val fragmentTransaction = childFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.fl_container, commentListFragment, commentListFragmentTag)
fragmentTransaction.addToBackStack(commentListFragmentTag)
fragmentTransaction.commit()
}
fun hideCommentDialog() {
dialog?.dismiss()
}
fun onClickComment(comment: GetAudioContentCommentListItem) {
val commentReplyFragmentTag = "COMMENT_REPLY_FRAGMENT"
val commentReplyFragment = AudioContentCommentReplyFragment.newInstance(
creatorId = creatorId,
audioContentId = audioContentId,
comment = comment
)
val fragmentTransaction = childFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.fl_container, commentReplyFragment, commentReplyFragmentTag)
fragmentTransaction.addToBackStack(commentReplyFragmentTag)
fragmentTransaction.commit()
}
}

View File

@@ -0,0 +1,215 @@
package kr.co.vividnext.sodalive.audio_content.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 AudioContentCommentListFragment : BaseFragment<FragmentAudioContentCommentListBinding>(
FragmentAudioContentCommentListBinding::inflate
) {
private val viewModel: AudioContentCommentListViewModel by inject()
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentCommentAdapter
private var creatorId: Long = 0
private var audioContentId: Long = 0
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
creatorId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID) ?: 0
audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireContext().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
setupView()
bindData()
viewModel.getCommentList(audioContentId = audioContentId) { hideDialog() }
}
private fun hideDialog() {
(parentFragment as AudioContentCommentFragment).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(audioContentId, comment)
}
adapter = AudioContentCommentAdapter(
creatorId = creatorId,
modifyComment = { commentId, comment ->
hideKeyboard()
viewModel.modifyComment(
commentId = commentId,
audioContentId = audioContentId,
comment = comment
)
},
onClickDelete = {
SodaDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
title = "댓글 삭제",
desc = "삭제하시겠습니까?",
confirmButtonTitle = "삭제",
confirmButtonClick = {
viewModel.modifyComment(
commentId = it,
audioContentId = audioContentId,
isActive = false
)
},
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
},
onItemClick = {
(parentFragment as AudioContentCommentFragment).onClickComment(it)
}
)
val recyclerView = binding.rvComment
recyclerView.setHasFixedSize(true)
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(audioContentId = audioContentId)
}
}
})
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, audioContentId: Long): AudioContentCommentListFragment {
val args = Bundle()
args.putLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, creatorId)
args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
val fragment = AudioContentCommentListFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@@ -0,0 +1,183 @@
package kr.co.vividnext.sodalive.audio_content.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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentCommentListViewModel(
private val repository: AudioContentCommentRepository
) : 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<GetAudioContentCommentListItem>>()
val commentList: LiveData<List<GetAudioContentCommentListItem>>
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(audioContentId: Long, onFailure: (() -> Unit)? = null) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentCommentList(
audioContentId = audioContentId,
page = page,
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(contentId: Long, comment: String) {
if (!_isLoading.value!!) {
_isLoading.value = true
}
compositeDisposable.add(
repository.registerComment(
contentId = contentId,
comment = comment,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
page = 1
isLast = false
getCommentList(contentId)
} 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,
audioContentId: Long,
comment: String? = null,
isActive: Boolean? = null
) {
if (comment == null && isActive == null) {
_toastLiveData.postValue("변경사항이 없습니다.")
return
}
if (comment != null && comment.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
return
}
_isLoading.value = true
val request = ModifyCommentRequest(commentId = commentId)
if (comment != null) {
request.comment = comment
}
if (isActive != null) {
request.isActive = isActive
}
compositeDisposable.add(
repository.modifyComment(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
page = 1
isLast = false
getCommentList(audioContentId)
} else {
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@@ -0,0 +1,220 @@
package kr.co.vividnext.sodalive.audio_content.comment
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentBinding
import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentReplyBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentCommentReplyAdapter(
private val creatorId: Long,
private val modifyComment: (Long, String) -> Unit,
private val onClickDelete: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentCommentReplyViewHolder>() {
var items = mutableSetOf<GetAudioContentCommentListItem>()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): AudioContentCommentReplyViewHolder {
return if (viewType == 0) {
AudioContentCommentReplyHeaderViewHolder(
binding = ItemAudioContentCommentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
)
} else {
AudioContentCommentReplyItemViewHolder(
context = parent.context,
creatorId = creatorId,
ItemAudioContentCommentReplyBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
showOptionMenu = { context, view, commentId, writerId, creatorId, onClickModify ->
showOptionMenu(context, view, commentId, writerId, creatorId, onClickModify)
},
modifyComment = modifyComment
)
}
}
override fun onBindViewHolder(holder: AudioContentCommentReplyViewHolder, 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 AudioContentCommentReplyViewHolder(
binding: ViewBinding
) : RecyclerView.ViewHolder(binding.root) {
abstract fun bind(item: GetAudioContentCommentListItem)
}
class AudioContentCommentReplyHeaderViewHolder(
private val binding: ItemAudioContentCommentBinding
) : AudioContentCommentReplyViewHolder(binding) {
override fun bind(item: GetAudioContentCommentListItem) {
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
val tvCommentLayoutParams = binding.tvComment.layoutParams as LinearLayout.LayoutParams
val can = item.donationCan
if (can > 0) {
tvCommentLayoutParams.topMargin = 0
binding.llDonationCan.visibility = View.VISIBLE
binding.tvDonationCan.text = can.moneyFormat()
binding.llDonationCan.setBackgroundResource(
when {
can >= 100000 -> {
R.drawable.bg_round_corner_10_7_973a3a
}
can >= 50000 -> {
R.drawable.bg_round_corner_10_7_d85e37
}
can >= 10000 -> {
R.drawable.bg_round_corner_10_7_d38c38
}
can >= 5000 -> {
R.drawable.bg_round_corner_10_7_59548f
}
can >= 1000 -> {
R.drawable.bg_round_corner_10_7_4d6aa4
}
can >= 500 -> {
R.drawable.bg_round_corner_10_7_2d7390
}
else -> {
R.drawable.bg_round_corner_10_7_548f7d
}
}
)
} else {
tvCommentLayoutParams.topMargin = 13.3f.dpToPx().toInt()
binding.llDonationCan.visibility = View.GONE
}
binding.tvComment.layoutParams = tvCommentLayoutParams
binding.tvComment.text = item.comment
binding.tvCommentDate.text = item.date
binding.tvCommentNickname.text = item.nickname
binding.tvWriteReply.visibility = View.GONE
binding.ivMenu.visibility = View.GONE
}
}
class AudioContentCommentReplyItemViewHolder(
private val context: Context,
private val creatorId: Long,
private val binding: ItemAudioContentCommentReplyBinding,
private val showOptionMenu: (
Context, View, Long, Long, Long, onClickModify: () -> Unit
) -> Unit,
private val modifyComment: (Long, String) -> Unit
) : AudioContentCommentReplyViewHolder(binding) {
override fun bind(item: GetAudioContentCommentListItem) {
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_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,238 @@
package kr.co.vividnext.sodalive.audio_content.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.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommentReplyBinding>(
FragmentAudioContentCommentReplyBinding::inflate
) {
private val viewModel: AudioContentCommentReplyViewModel by inject()
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentCommentReplyAdapter
private var originalComment: GetAudioContentCommentListItem? = null
private var creatorId: Long = 0
private var audioContentId: Long = 0
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
creatorId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID) ?: 0
audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0
originalComment = BundleCompat.getParcelable(
requireArguments(),
Constants.EXTRA_AUDIO_CONTENT_COMMENT,
GetAudioContentCommentListItem::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 AudioContentCommentFragment).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(audioContentId, originalComment!!.id, comment)
}
adapter = AudioContentCommentReplyAdapter(
creatorId = creatorId,
modifyComment = { commentId, comment ->
hideKeyboard()
viewModel.modifyComment(
commentId = commentId,
parentCommentId = originalComment!!.id,
comment = comment
)
},
onClickDelete = {
SodaDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
title = "댓글 삭제",
desc = "삭제하시겠습니까?",
confirmButtonTitle = "삭제",
confirmButtonClick = {
viewModel.modifyComment(
commentId = it,
parentCommentId = originalComment!!.id,
isActive = false
)
},
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
}
).apply {
items.add(originalComment!!)
}
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,
audioContentId: Long,
comment: GetAudioContentCommentListItem
): AudioContentCommentReplyFragment {
val args = Bundle()
args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
args.putLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, creatorId)
args.putParcelable(Constants.EXTRA_AUDIO_CONTENT_COMMENT, comment)
val fragment = AudioContentCommentReplyFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@@ -0,0 +1,178 @@
package kr.co.vividnext.sodalive.audio_content.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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentCommentReplyViewModel(
private val repository: AudioContentCommentRepository
) : 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<GetAudioContentCommentListItem>>()
val commentList: LiveData<List<GetAudioContentCommentListItem>>
get() = _commentList
var page = 1
private var isLast = false
private val size = 10
fun getCommentReplyList(commentId: Long, onFailure: (() -> Unit)? = null) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentCommentReplyList(
commentId = commentId,
page = page,
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()
}
}
)
)
}
}
fun registerComment(contentId: Long, commentId: Long, comment: String) {
if (!_isLoading.value!!) {
_isLoading.value = true
}
compositeDisposable.add(
repository.registerComment(
contentId = contentId,
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
getCommentReplyList(commentId)
} 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,
parentCommentId: Long,
comment: String? = null,
isActive: Boolean? = null
) {
if (comment == null && isActive == null) {
_toastLiveData.postValue("변경사항이 없습니다.")
return
}
if (comment != null && comment.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
return
}
_isLoading.value = true
val request = ModifyCommentRequest(commentId = commentId)
if (comment != null) {
request.comment = comment
}
if (isActive != null) {
request.isActive = isActive
}
compositeDisposable.add(
repository.modifyComment(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
page = 1
isLast = false
getCommentReplyList(parentCommentId)
} else {
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@@ -0,0 +1,51 @@
package kr.co.vividnext.sodalive.audio_content.comment
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
import java.util.TimeZone
class AudioContentCommentRepository(private val api: AudioContentApi) {
fun registerComment(
contentId: Long,
comment: String,
parentId: Long? = null,
token: String
) = api.registerComment(
request = RegisterAudioContentCommentRequest(
comment = comment,
contentId = contentId,
parentId = parentId
),
authHeader = token
)
fun getAudioContentCommentList(
audioContentId: Long,
page: Int,
size: Int,
token: String
) = api.getAudioContentCommentList(
id = audioContentId,
page = page - 1,
size = size,
timezone = TimeZone.getDefault().id,
authHeader = token
)
fun getAudioContentCommentReplyList(
commentId: Long,
page: Int,
size: Int,
token: String
) = api.getAudioContentCommentCommentList(
id = commentId,
page = page - 1,
size = size,
timezone = TimeZone.getDefault().id,
authHeader = token
)
fun modifyComment(request: ModifyCommentRequest, token: String) = api.modifyComment(
request = request,
authHeader = token
)
}

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.comment
import com.google.gson.annotations.SerializedName
data class RegisterAudioContentCommentRequest(
@SerializedName("comment") val comment: String,
@SerializedName("contentId") val contentId: Long,
@SerializedName("parentId") val parentId: Long?
)

View File

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

View File

@@ -0,0 +1,86 @@
package kr.co.vividnext.sodalive.audio_content.curation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentCurationViewModel(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _contentListLiveData = MutableLiveData<GetCurationContentResponse>()
val contentListLiveData: LiveData<GetCurationContentResponse>
get() = _contentListLiveData
private val _sort = MutableLiveData(AudioContentViewModel.Sort.NEWEST)
val sort: LiveData<AudioContentViewModel.Sort>
get() = _sort
private var isLast = false
var page = 1
private val size = 10
fun getContentList(curationId: Long) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentListByCurationId(
curationId = curationId,
page = page,
size = size,
sort = _sort.value!!,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
page += 1
_contentListLiveData.postValue(it.data!!)
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
fun changeSort(sort: AudioContentViewModel.Sort) {
page = 1
isLast = false
_sort.postValue(sort)
}
}

View File

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

View File

@@ -0,0 +1,67 @@
package kr.co.vividnext.sodalive.audio_content.detail
import android.annotation.SuppressLint
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.WindowManager
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import kr.co.vividnext.sodalive.databinding.DialogAudioContentDeleteBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
@SuppressLint("SetTextI18n")
class AudioContentDeleteDialog(
activity: Activity,
layoutInflater: LayoutInflater,
title: String,
confirmButtonClick: () -> Unit
) {
private val alertDialog: AlertDialog
val dialogView = DialogAudioContentDeleteBinding.inflate(layoutInflater)
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialogView.tvTitle.text = "[$title]을 삭제하시겠습니까?"
dialogView.tvCancel.setOnClickListener {
alertDialog.dismiss()
}
dialogView.tvConfirm.setOnClickListener {
if (dialogView.tvNotice.isSelected) {
alertDialog.dismiss()
confirmButtonClick()
} else {
Toast.makeText(
activity,
"동의하셔야 삭제할 수 있습니다.",
Toast.LENGTH_LONG
).show()
}
}
dialogView.tvNotice.setOnClickListener {
it.isSelected = !it.isSelected
}
}
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,822 @@
package kr.co.vividnext.sodalive.audio_content.detail
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.RelativeLayout
import android.widget.SeekBar
import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.google.gson.Gson
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentFragment
import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyActivity
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderConfirmDialog
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderFragment
import kr.co.vividnext.sodalive.audio_content.order.OrderType
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.Utils
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentDetailBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog
import kr.co.vividnext.sodalive.mypage.auth.Auth
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
import kr.co.vividnext.sodalive.report.ReportType
import org.koin.android.ext.android.inject
class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBinding>(
ActivityAudioContentDetailBinding::inflate
) {
private val viewModel: AudioContentDetailViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var creatorOtherContentAdapter: OtherContentAdapter
private lateinit var sameThemeOtherContentAdapter: OtherContentAdapter
private var audioContentId: Long = 0
private var isAlertPreview = false
private val audioContentReceiver = AudioContentReceiver()
private var creatorId: Long = 0
private var refresh = false
set(value) {
field = value
setResult(RESULT_OK)
}
private var title = ""
@SuppressLint("SetTextI18n")
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
binding.scrollView.scrollTo(0, 0)
binding.sbProgress.progress = 0
binding.ivPlayOrPause.setImageResource(0)
binding.tvTotalDuration.text = " / 00:00:00"
binding.tvCurrentDuration.text = "00:00:00"
binding.rlPreviewAlert.visibility = View.GONE
audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
override fun onCreate(savedInstanceState: Bundle?) {
audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
super.onCreate(savedInstanceState)
if (audioContentId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
bindData()
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
override fun onResume() {
super.onResume()
val intentFilter = IntentFilter(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
registerReceiver(audioContentReceiver, intentFilter)
if (refresh) {
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
}
override fun onPause() {
unregisterReceiver(audioContentReceiver)
super.onPause()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.tvBack.text = "콘텐츠 상세"
binding.tvBack.setOnClickListener { finish() }
binding.ivClosePreviewAlert.setOnClickListener { viewModel.toggleShowPreviewAlert() }
binding.ivMenu.setOnClickListener {
showOptionMenu(
this,
binding.ivMenu,
)
}
creatorOtherContentAdapter = OtherContentAdapter {
val intent = Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
startActivity(intent)
}
binding.rvCreatorOtherContent.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.HORIZONTAL,
false
)
binding.swipeRefreshLayout.setOnRefreshListener {
viewModel.getAudioContentDetail(
audioContentId = audioContentId
) { finish() }
binding.swipeRefreshLayout.isRefreshing = false
}
binding.rvCreatorOtherContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
creatorOtherContentAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvCreatorOtherContent.adapter = creatorOtherContentAdapter
sameThemeOtherContentAdapter = OtherContentAdapter {
val intent = Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
startActivity(intent)
}
binding.rvThemeOtherContent.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvThemeOtherContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
creatorOtherContentAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvThemeOtherContent.adapter = sameThemeOtherContentAdapter
binding.sbProgress.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
}
override fun onStartTrackingTouch(p0: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
if (seekBar != null) {
val intent = Intent(
this@AudioContentDetailActivity,
AudioContentPlayService::class.java
)
intent.action = AudioContentPlayService.MusicAction.PROGRESS.name
intent.putExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, seekBar.progress)
startService(intent)
}
}
})
val layoutParams = binding.ivCover.layoutParams as RelativeLayout.LayoutParams
layoutParams.width = (screenWidth - 13.3f.dpToPx()).toInt()
layoutParams.height = (screenWidth - 13.3f.dpToPx()).toInt()
binding.ivCover.layoutParams = layoutParams
binding.ivPlayLoop.setOnClickListener { viewModel.togglePlayLoop() }
binding.llDonation.setOnClickListener {
val dialog = LiveRoomDonationDialog(
this,
LayoutInflater.from(this)
) { can, message ->
if (can <= 0) {
showToast("1캔 이상 후원하실 수 있습니다.")
} else if (message.isBlank()) {
showToast("함께 보낼 메시지를 입력하세요.")
} else {
donation(can, message)
}
}
dialog.show(screenWidth)
}
}
private fun donation(can: Int, message: String) {
viewModel.donation(audioContentId, can, message) {
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
}
private fun showOptionMenu(context: Context, v: View) {
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
if (
viewModel.audioContentLiveData.value!!.creator.creatorId ==
SharedPreferenceManager.userId
) {
inflater.inflate(R.menu.audio_content_detail_creator_menu, popup.menu)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_modify -> {
refresh = true
startActivity(
Intent(applicationContext, AudioContentModifyActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
}
)
}
R.id.menu_delete -> {
showDeleteDialog()
}
}
true
}
} else {
inflater.inflate(R.menu.audio_content_detail_user_menu, popup.menu)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_report -> {
showReportDialog()
}
}
true
}
}
popup.show()
}
private fun showDeleteDialog() {
AudioContentDeleteDialog(
this,
layoutInflater,
this.title,
confirmButtonClick = {
viewModel.deleteAudioContent(audioContentId) {
setResult(RESULT_OK)
finish()
}
}
).show(screenWidth)
}
private fun showReportDialog() {
AudioContentReportDialog(this, layoutInflater) {
viewModel.report(
type = ReportType.AUDIO_CONTENT,
contentId = audioContentId,
reason = it
)
}.show(screenWidth)
}
@SuppressLint("NotifyDataSetChanged", "SetTextI18n")
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.isExpandDetail.observe(this) {
binding.tvDetail.maxLines = if (it) {
Int.MAX_VALUE
} else {
2
}
}
viewModel.isShowPreviewAlert.observe(this) {
binding.rlPreviewAlert.visibility = if (it) {
View.VISIBLE
} else {
View.GONE
}
}
viewModel.audioContentLiveData.observe(this) {
refresh = false
startService(
Intent(this, AudioContentPlayService::class.java).apply {
action = AudioContentPlayService.MusicAction.INIT.name
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it.contentId)
}
)
title = it.title
setupCreatorArea(it.creator)
setupMosaicArea(it.isMosaic)
setupPlayArea(it)
setupInfoArea(it)
setupPurchaseButton(it)
setupCommentArea(it)
setupCreatorOtherContentListArea(it.creatorOtherContentList)
setupSameThemeOtherContentList(it.sameThemeOtherContentList)
isAlertPreview = it.creator.creatorId != SharedPreferenceManager.userId &&
!it.existOrdered &&
it.price > 0
binding.ivPlayOrPause.setImageResource(
if (isAlertPreview) {
R.drawable.btn_audio_content_preview_play
} else {
R.drawable.btn_audio_content_play
}
)
}
viewModel.isContentPlayLoopLiveData.observe(this) {
if (it) {
binding.ivPlayLoop.setImageResource(R.drawable.btn_player_repeat)
} else {
binding.ivPlayLoop.setImageResource(R.drawable.btn_player_repeat_done)
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setupSameThemeOtherContentList(
sameThemeOtherContentList: List<OtherContentResponse>
) {
if (sameThemeOtherContentList.isEmpty()) {
binding.rvThemeOtherContent.visibility = View.GONE
binding.llThemeOtherContentPreparing.visibility = View.VISIBLE
} else {
binding.rvThemeOtherContent.visibility = View.VISIBLE
binding.llThemeOtherContentPreparing.visibility = View.GONE
sameThemeOtherContentAdapter.items.clear()
sameThemeOtherContentAdapter.items.addAll(sameThemeOtherContentList)
sameThemeOtherContentAdapter.notifyDataSetChanged()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setupCreatorOtherContentListArea(
creatorOtherContentList: List<OtherContentResponse>
) {
if (creatorOtherContentList.isEmpty()) {
binding.rvCreatorOtherContent.visibility = View.GONE
binding.llCreatorOtherContentPreparing.visibility = View.VISIBLE
} else {
binding.rvCreatorOtherContent.visibility = View.VISIBLE
binding.llCreatorOtherContentPreparing.visibility = View.GONE
creatorOtherContentAdapter.items.clear()
creatorOtherContentAdapter.items.addAll(creatorOtherContentList)
creatorOtherContentAdapter.notifyDataSetChanged()
}
}
private fun setupCommentArea(response: GetAudioContentDetailResponse) {
if (response.isCommentAvailable) {
binding.llDonation.visibility = View.VISIBLE
binding.llComment.visibility = View.VISIBLE
binding.tvCommentCount.text = "${response.commentCount}"
if (response.commentCount > 0) {
binding.ivCommentProfile.load(response.commentList[0].profileUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
binding.tvCommentText.text = response.commentList[0].comment
binding.tvCommentText.visibility = View.VISIBLE
binding.rlInputComment.visibility = View.GONE
binding.llComment.setOnClickListener { showCommentBottomSheetDialog() }
} 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("")
viewModel.registerComment(audioContentId, comment)
}
binding.llComment.setOnClickListener {}
}
} else {
binding.llComment.visibility = View.GONE
binding.llDonation.visibility = View.GONE
}
}
private fun showCommentBottomSheetDialog() {
val dialog = AudioContentCommentFragment(
creatorId = creatorId,
audioContentId = audioContentId
)
dialog.show(
supportFragmentManager,
dialog.tag
)
}
private fun setupPurchaseButton(response: GetAudioContentDetailResponse) {
if (
response.price > 0 &&
!response.existOrdered &&
response.orderType == null &&
response.creator.creatorId != SharedPreferenceManager.userId
) {
binding.llPurchase.visibility = View.VISIBLE
binding.tvPrice.text = response.price.toString()
binding.tvStrPurchaseOrRental.text = if (response.isOnlyRental) {
" 대여하기"
} else {
" 구매하기"
}
binding.llPurchase.setOnClickListener {
showOrderDialog(audioContent = response, isOnlyRental = response.isOnlyRental)
}
} else {
binding.llPurchase.visibility = View.GONE
}
}
@SuppressLint("SetTextI18n")
private fun setupPlayArea(response: GetAudioContentDetailResponse) {
Glide
.with(this)
.load(response.coverImageUrl)
.centerCrop()
.placeholder(R.drawable.bg_black)
.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
)
}
)
}
binding.tvTotalDuration.text = " / ${response.duration}"
}
@SuppressLint("SetTextI18n")
private fun setupInfoArea(response: GetAudioContentDetailResponse) {
binding.tvTheme.text = response.themeStr
binding.tv19.visibility = if (response.isAdult) {
View.VISIBLE
} else {
View.GONE
}
if (response.orderType != null && response.orderType == OrderType.KEEP) {
binding.tvPurchased.visibility = View.VISIBLE
binding.tvRental.visibility = View.GONE
binding.tvRemainingTime.visibility = View.GONE
} else if (response.orderType != null && response.orderType == OrderType.RENTAL) {
binding.tvPurchased.visibility = View.GONE
binding.tvRental.visibility = View.VISIBLE
binding.tvRemainingTime.visibility = View.VISIBLE
binding.tvRemainingTime.text = response.remainingTime
} else {
binding.tvPurchased.visibility = View.GONE
binding.tvRental.visibility = View.GONE
binding.tvRemainingTime.visibility = View.GONE
}
binding.tvTitle.text = response.title
binding.tvDetail.text = response.detail
binding.tvDetail.setOnClickListener { viewModel.toggleExpandDetail() }
if (response.tag.isNotBlank()) {
binding.tvTag.visibility = View.VISIBLE
binding.tvTag.text = response.tag
} else {
binding.tvTag.visibility = View.GONE
}
binding.ivLike.setImageResource(
if (response.isLike) {
R.drawable.ic_audio_content_heart_pressed
} else {
R.drawable.ic_audio_content_heart_normal
}
)
binding.tvLike.text = "${response.likeCount}"
binding.llLike.setOnClickListener {
viewModel.likeContent(contentId = audioContentId) {
val likeCount = binding.tvLike.text.toString().toInt()
if (it) {
binding.tvLike.text = "${likeCount + 1}"
binding.ivLike.setImageResource(R.drawable.ic_audio_content_heart_pressed)
} else {
binding.tvLike.text = if (likeCount - 1 < 0) {
"0"
} else {
"${likeCount - 1}"
}
binding.ivLike.setImageResource(R.drawable.ic_audio_content_heart_normal)
}
}
}
binding.tvShare.setOnClickListener {
viewModel.shareAudioContent(
audioContentId = audioContentId,
contentImage = response.coverImageUrl,
contentTitle = "${response.title} - ${response.creator.nickname}"
) {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, it)
val shareIntent = Intent.createChooser(intent, "오디오콘텐츠 공유")
startActivity(shareIntent)
}
}
}
private fun setupMosaicArea(isMosaic: Boolean) {
if (isMosaic) {
binding.alert19Bg.visibility = View.VISIBLE
binding.llAlert19.visibility = View.VISIBLE
binding.tvAuth.setOnClickListener {
Auth.auth(this, applicationContext) { data ->
val bootpayResponse = Gson().fromJson(data, BootpayResponse::class.java)
val request = AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId)
runOnUiThread {
viewModel.authVerify(audioContentId = audioContentId, request = request)
}
}
}
} else {
binding.alert19Bg.visibility = View.GONE
binding.llAlert19.visibility = View.GONE
binding.tvAuth.setOnClickListener {}
}
}
private fun setupCreatorArea(creator: AudioContentCreator) {
this.creatorId = creator.creatorId
binding.rlProfile.setOnClickListener {
startActivity(
Intent(applicationContext, UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, creator.creatorId)
}
)
}
binding.ivProfile.load(creator.profileImageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
binding.tvProfileNickname.text = creator.nickname
if (creator.creatorId != SharedPreferenceManager.userId) {
binding.ivFollow.visibility = View.VISIBLE
if (creator.isFollowing) {
binding.ivFollow.setImageResource(R.drawable.btn_notification_selected)
binding.ivFollow.setOnClickListener {
viewModel.unRegisterNotification(
contentId = audioContentId,
creatorId = creator.creatorId
)
}
} else {
binding.ivFollow.setImageResource(R.drawable.btn_notification)
binding.ivFollow.setOnClickListener {
viewModel.registerNotification(
contentId = audioContentId,
creatorId = creator.creatorId
)
}
}
} else {
binding.ivFollow.visibility = View.GONE
}
}
private fun showOrderDialog(
audioContent: GetAudioContentDetailResponse,
isOnlyRental: Boolean = false
) {
val dialog = AudioContentOrderFragment(
price = audioContent.price,
isOnlyRental = isOnlyRental,
onClickKeep = { showOrderConfirmDialog(audioContent, isOnlyRental, OrderType.KEEP) },
onClickRental = { showOrderConfirmDialog(audioContent, isOnlyRental, OrderType.RENTAL) }
)
dialog.show(
supportFragmentManager,
dialog.tag
)
}
private fun showOrderConfirmDialog(
audioContent: GetAudioContentDetailResponse,
isOnlyRental: Boolean = false,
orderType: OrderType
) {
AudioContentOrderConfirmDialog(
activity = this,
layoutInflater = layoutInflater,
title = audioContent.title,
theme = audioContent.themeStr,
coverImageUrl = audioContent.coverImageUrl,
profileImageUrl = audioContent.creator.profileImageUrl,
nickname = audioContent.creator.nickname,
duration = audioContent.duration,
isOnlyRental = isOnlyRental,
orderType = orderType,
price = audioContent.price,
confirmButtonClick = {
startService(
Intent(this, AudioContentPlayService::class.java).apply {
action = AudioContentPlayService.MusicAction.CONDITIONAL_STOP.name
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContent.contentId)
}
)
binding.rlPreviewAlert.visibility = View.GONE
viewModel.order(
contentId = audioContent.contentId,
orderType = orderType
) {
val intent = Intent(applicationContext, CanChargeActivity::class.java)
intent.putExtra(Constants.EXTRA_GO_TO_PREV_PAGE, true)
startActivity(intent)
}
},
).show(screenWidth)
}
inner class AudioContentReceiver : BroadcastReceiver() {
@SuppressLint("SetTextI18n")
override fun onReceive(context: Context?, intent: Intent?) {
val nextAction = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent?.getSerializableExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
AudioContentPlayService.MusicAction::class.java
)
} else {
intent?.getSerializableExtra(Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION)
}
val duration = intent?.getIntExtra(Constants.EXTRA_AUDIO_CONTENT_DURATION, 0)
val progress = intent?.getIntExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, 0)
val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
val isPlaying = intent?.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYING, false)
val changeUi = intent?.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI, false)
val alertPreview = intent?.getBooleanExtra(
Constants.EXTRA_AUDIO_CONTENT_ALERT_PREVIEW,
false
)
val isLoading = intent?.getBooleanExtra(
Constants.EXTRA_AUDIO_CONTENT_LOADING,
false
)
viewModel.isLoading.value = isLoading ?: false
if (this@AudioContentDetailActivity.audioContentId == contentId) {
runOnUiThread {
if (changeUi != null && changeUi) {
binding.ivPlayOrPause.setImageResource(
if (isPlaying != null && isPlaying) {
R.drawable.btn_audio_content_pause
} else {
if (isAlertPreview) {
R.drawable.btn_audio_content_preview_play
} else {
R.drawable.btn_audio_content_play
}
}
)
}
}
if (duration != null && duration > 0) {
binding.sbProgress.max = duration
binding.tvTotalDuration.text = " / ${Utils.convertDurationToString(duration)}"
}
if (progress != null && progress > 0) {
binding.sbProgress.progress = progress
binding.tvCurrentDuration.text = Utils.convertDurationToString(progress)
}
if (alertPreview != null && alertPreview && isAlertPreview) {
viewModel.toggleShowPreviewAlert()
}
if (nextAction != null) {
startService(
Intent(
this@AudioContentDetailActivity,
AudioContentPlayService::class.java
).apply {
action = (nextAction as AudioContentPlayService.MusicAction).name
}
)
}
}
}
}
}

View File

@@ -0,0 +1,484 @@
package kr.co.vividnext.sodalive.audio_content.detail
import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.firebase.dynamiclinks.ShortDynamicLink
import com.google.firebase.dynamiclinks.ktx.androidParameters
import com.google.firebase.dynamiclinks.ktx.dynamicLinks
import com.google.firebase.dynamiclinks.ktx.iosParameters
import com.google.firebase.dynamiclinks.ktx.shortLinkAsync
import com.google.firebase.dynamiclinks.ktx.socialMetaTagParameters
import com.google.firebase.ktx.Firebase
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.comment.AudioContentCommentRepository
import kr.co.vividnext.sodalive.audio_content.order.OrderType
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.mypage.auth.AuthRepository
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.report.ReportRequest
import kr.co.vividnext.sodalive.report.ReportType
class AudioContentDetailViewModel(
private val repository: AudioContentRepository,
private val authRepository: AuthRepository,
private val reportRepository: ReportRepository,
private val commentRepository: AudioContentCommentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
var isLoading = MutableLiveData(false)
private set
private var _audioContentLiveData = MutableLiveData<GetAudioContentDetailResponse>()
val audioContentLiveData: LiveData<GetAudioContentDetailResponse>
get() = _audioContentLiveData
private val _isExpandDetail = MutableLiveData(false)
val isExpandDetail: LiveData<Boolean>
get() = _isExpandDetail
private val _isShowPreviewAlert = MutableLiveData(false)
val isShowPreviewAlert: LiveData<Boolean>
get() = _isShowPreviewAlert
private val _isContentPlayLoopLiveData = MutableLiveData(
SharedPreferenceManager.isContentPlayLoop
)
val isContentPlayLoopLiveData: LiveData<Boolean>
get() = _isContentPlayLoopLiveData
fun getAudioContentDetail(audioContentId: Long, onFailure: (() -> Unit)? = null) {
if (!isLoading.value!!) {
isLoading.value = true
}
compositeDisposable.add(
repository.getAudioContentDetail(
audioContentId = audioContentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_audioContentLiveData.postValue(it.data!!)
} 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 registerNotification(contentId: Long, creatorId: Long) {
isLoading.value = true
compositeDisposable.add(
repository.registerNotification(
creatorId,
"Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
getAudioContentDetail(contentId)
} 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 unRegisterNotification(contentId: Long, creatorId: Long) {
isLoading.value = true
compositeDisposable.add(
repository.unRegisterNotification(
creatorId,
"Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
getAudioContentDetail(contentId)
} 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 toggleExpandDetail() {
_isExpandDetail.value = !_isExpandDetail.value!!
}
fun toggleShowPreviewAlert() {
_isShowPreviewAlert.value = !_isShowPreviewAlert.value!!
}
fun order(contentId: Long, orderType: OrderType, gotoShop: () -> Unit) {
isLoading.value = true
compositeDisposable.add(
repository.orderContent(
contentId = contentId,
orderType = orderType,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
getAudioContentDetail(audioContentId = contentId)
_toastLiveData.postValue(
if (orderType == OrderType.RENTAL) {
"대여가 완료되었습니다."
} else {
"구매가 완료되었습니다."
}
)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
if (it.message.contains("캔이 부족합니다")) {
gotoShop()
}
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
isLoading.value = false
},
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun authVerify(audioContentId: Long, request: AuthVerifyRequest) {
if (!isLoading.value!!) {
isLoading.value = true
}
compositeDisposable.add(
authRepository.verify(request, "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
getAudioContentDetail(audioContentId)
} 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 registerComment(audioContentId: Long, comment: String) {
if (!isLoading.value!!) {
isLoading.value = true
}
compositeDisposable.add(
commentRepository.registerComment(
contentId = audioContentId,
comment = comment,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
getAudioContentDetail(audioContentId)
} 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 likeContent(contentId: Long, onSuccess: (Boolean) -> Unit) {
if (!isLoading.value!!) {
isLoading.value = true
}
compositeDisposable.add(
repository.likeContent(
request = PutAudioContentLikeRequest(contentId = contentId),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
isLoading.value = false
if (it.success) {
if (it.data != null) {
onSuccess(it.data.like)
} else {
getAudioContentDetail(contentId)
}
} 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 shareAudioContent(
audioContentId: Long,
contentImage: String,
contentTitle: String,
onSuccess: (String) -> Unit
) {
isLoading.value = true
Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) {
link = Uri.parse("https://sodalive.net/?audio_content_id=$audioContentId")
domainUriPrefix = "https://sodalive.page.link"
androidParameters { }
iosParameters("kr.co.vividnext.sodalive") {
appStoreId = "6461721697"
}
socialMetaTagParameters {
title = contentTitle
description = "지금 소다라이브에서 이 콘텐츠 감상하기"
imageUrl = contentImage.toUri()
}
}.addOnSuccessListener {
val uri = it.shortLink
if (uri != null) {
val message = uri.toString()
onSuccess(message)
} else {
_toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.")
}
}.addOnFailureListener {
_toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.")
}.addOnCompleteListener {
isLoading.value = false
}
}
fun deleteAudioContent(audioContentId: Long, onSuccess: () -> Unit) {
isLoading.value = true
compositeDisposable.add(
repository.deleteAudioContent(
audioContentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
isLoading.value = false
if (it.success) {
_toastLiveData.postValue(
"삭제되었습니다."
)
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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun report(type: ReportType, contentId: Long, reason: String) {
isLoading.value = true
val request = ReportRequest(type = type, reason = reason, contentId = contentId)
compositeDisposable.add(
reportRepository.report(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"신고가 접수되었습니다."
)
}
isLoading.value = false
},
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("신고가 접수되었습니다.")
}
)
)
}
fun togglePlayLoop() {
val isPlayLoop = !SharedPreferenceManager.isContentPlayLoop
SharedPreferenceManager.isContentPlayLoop = isPlayLoop
_isContentPlayLoopLiveData.value = isPlayLoop
}
fun donation(contentId: Long, can: Int, comment: String, onSuccess: () -> Unit) {
isLoading.value = true
compositeDisposable.add(
repository.donation(
contentId = contentId,
can = can,
comment = comment,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
isLoading.value = false
if (it.success) {
SharedPreferenceManager.can -= can
_toastLiveData.postValue(
"${can.moneyFormat()}캔을 후원하였습니다."
)
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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@@ -0,0 +1,60 @@
package kr.co.vividnext.sodalive.audio_content.detail
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.DialogAudioContentReportBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentReportDialog(
activity: Activity,
layoutInflater: LayoutInflater,
confirmButtonClick: (String) -> Unit
) {
private val alertDialog: AlertDialog
val dialogView = DialogAudioContentReportBinding.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,46 @@
package kr.co.vividnext.sodalive.audio_content.detail
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListItem
import kr.co.vividnext.sodalive.audio_content.order.OrderType
data class GetAudioContentDetailResponse(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@SerializedName("detail") val detail: String,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("contentUrl") val contentUrl: String,
@SerializedName("themeStr") val themeStr: String,
@SerializedName("tag") val tag: String,
@SerializedName("price") val price: Int,
@SerializedName("duration") val duration: String,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isMosaic") val isMosaic: Boolean,
@SerializedName("isOnlyRental") val isOnlyRental: Boolean,
@SerializedName("existOrdered") val existOrdered: Boolean,
@SerializedName("orderType") val orderType: OrderType?,
@SerializedName("remainingTime") val remainingTime: String?,
@SerializedName("creatorOtherContentList")
val creatorOtherContentList: List<OtherContentResponse>,
@SerializedName("sameThemeOtherContentList")
val sameThemeOtherContentList: List<OtherContentResponse>,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean,
@SerializedName("isLike") val isLike: Boolean,
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentList") val commentList: List<GetAudioContentCommentListItem>,
@SerializedName("commentCount") val commentCount: Int,
@SerializedName("creator") val creator: AudioContentCreator
)
data class OtherContentResponse(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverUrl") val coverUrl: String,
)
data class AudioContentCreator(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImageUrl") val profileImageUrl: String,
@SerializedName("isFollowing") val isFollowing: Boolean
)

View File

@@ -0,0 +1,46 @@
package kr.co.vividnext.sodalive.audio_content.detail
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioOtherContentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class OtherContentAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<OtherContentAdapter.ViewHolder>() {
val items = mutableListOf<OtherContentResponse>()
inner class ViewHolder(
private val binding: ItemAudioOtherContentBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: OtherContentResponse) {
binding.ivCover.load(item.coverUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(2.7f.dpToPx()))
}
binding.tvTitle.text = item.title
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAudioOtherContentBinding.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

@@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.audio_content.detail
import com.google.gson.annotations.SerializedName
data class PutAudioContentLikeRequest(
@SerializedName("contentId") val contentId: Long
)
data class PutAudioContentLikeResponse(
@SerializedName("like") val like: Boolean
)

View File

@@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.audio_content.donation
import com.google.gson.annotations.SerializedName
data class AudioContentDonationRequest(
@SerializedName("contentId") val contentId: Long,
@SerializedName("donationCan") val donationCan: Int,
@SerializedName("comment") val comment: String,
@SerializedName("container") val container: String = "aos"
)

View File

@@ -0,0 +1,53 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.widget.FrameLayout
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.bannerview.BaseViewHolder
import kr.co.vividnext.sodalive.R
class AudioContentMainBannerAdapter(
private val context: Context,
private val itemWidth: Int,
private val itemHeight: Int,
private val onClick: (GetAudioContentBannerResponse) -> Unit
) : BaseBannerAdapter<GetAudioContentBannerResponse>() {
override fun bindData(
holder: BaseViewHolder<GetAudioContentBannerResponse>,
data: GetAudioContentBannerResponse,
position: Int,
pageSize: Int
) {
val ivBanner = holder.findViewById<ImageView>(R.id.iv_recommend_live)
val layoutParams = ivBanner.layoutParams as FrameLayout.LayoutParams
layoutParams.width = itemWidth
layoutParams.height = itemHeight
Glide
.with(context)
.asBitmap()
.load(data.thumbnailImageUrl)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
ivBanner.setImageBitmap(resource)
ivBanner.layoutParams = layoutParams
}
override fun onLoadCleared(placeholder: Drawable?) {
}
})
ivBanner.setOnClickListener { onClick(data) }
}
override fun getLayoutId(viewType: Int): Int {
return R.layout.item_recommend_live
}
}

View File

@@ -0,0 +1,41 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainBinding
class AudioContentMainContentAdapter(
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit,
) : RecyclerView.Adapter<AudioContentMainItemViewHolder>() {
private val items = mutableListOf<GetAudioContentMainItem>()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = AudioContentMainItemViewHolder(
ItemAudioContentMainBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
onClickItem = onClickItem,
onClickCreator = onClickCreator
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: AudioContentMainItemViewHolder, position: Int) {
holder.bind(items[position])
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetAudioContentMainItem>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,96 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainCurationBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainCurationAdapter(
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit,
private val onClickCurationMore: (Long, String) -> Unit
) : RecyclerView.Adapter<AudioContentMainCurationAdapter.ViewHolder>() {
private val items = mutableListOf<GetAudioContentCurationResponse>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentMainCurationBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentCurationResponse) {
binding.tvTitle.text = item.title
binding.tvDesc.text = item.description
binding.ivAll.setOnClickListener { onClickCurationMore(item.curationId, item.title) }
setAudioContentList(item.audioContents)
}
private fun setAudioContentList(audioContents: List<GetAudioContentMainItem>) {
val adapter = AudioContentMainContentAdapter(onClickItem, onClickCreator)
binding.rvCuration.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
if (binding.rvCuration.itemDecorationCount == 0) {
binding.rvCuration.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
}
binding.rvCuration.adapter = adapter
adapter.addItems(audioContents)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemAudioContentMainCurationBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetAudioContentCurationResponse>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,595 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.app.Service
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.pointclick.sdk.offerwall.core.PointClickAd
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllActivity
import kr.co.vividnext.sodalive.audio_content.all.AudioContentRankingAllActivity
import kr.co.vividnext.sodalive.audio_content.curation.AudioContentCurationActivity
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
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
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.notification.MemberRole
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
FragmentAudioContentMainBinding::inflate
) {
private val viewModel: AudioContentMainViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var imm: InputMethodManager
private lateinit var newContentCreatorAdapter: AudioContentMainNewContentCreatorAdapter
private lateinit var bannerAdapter: AudioContentMainBannerAdapter
private lateinit var orderListAdapter: AudioContentMainContentAdapter
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentAdapter: AudioContentMainContentAdapter
private lateinit var contentRankingSortAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var contentRankingAdapter: AudioContentMainRankingAdapter
private lateinit var curationAdapter: AudioContentMainCurationAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireContext().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
setupView()
bindData()
viewModel.getMain()
}
private fun setupView() {
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
binding.llUploadContent.visibility = View.VISIBLE
binding.llUploadContent.setOnClickListener {
startActivity(
Intent(
requireActivity(),
AudioContentUploadActivity::class.java
)
)
}
} else {
binding.llUploadContent.visibility = View.GONE
}
setupNewContentCreator()
setupBanner()
setupOrderList()
setupNewContentTheme()
setupNewContent()
setupContentRankingSortType()
setupContentRanking()
setupCuration()
binding.swipeRefreshLayout.setOnRefreshListener {
binding.swipeRefreshLayout.isRefreshing = false
viewModel.getMain()
}
binding.ivCanFree.setOnClickListener {
PointClickAd.showOfferwall(requireActivity(), "무료충전")
}
}
private fun setupNewContentCreator() {
newContentCreatorAdapter = AudioContentMainNewContentCreatorAdapter {
val intent = Intent(requireContext(), UserProfileActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, it)
startActivity(intent)
}
binding.rvNewContentCreator.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContentCreator.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 10.7f.dpToPx().toInt()
}
newContentCreatorAdapter.itemCount - 1 -> {
outRect.left = 10.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 10.7f.dpToPx().toInt()
outRect.right = 10.7f.dpToPx().toInt()
}
}
}
})
binding.rvNewContentCreator.adapter = newContentCreatorAdapter
}
private fun setupBanner() {
val layoutParams = binding
.rvBanner
.layoutParams as LinearLayout.LayoutParams
val pagerWidth = screenWidth.toDouble() - 26.7f.dpToPx()
val pagerHeight = (pagerWidth * 0.53).roundToInt()
layoutParams.width = pagerWidth.roundToInt()
layoutParams.height = pagerHeight
bannerAdapter = AudioContentMainBannerAdapter(
requireContext(),
pagerWidth.roundToInt(),
pagerHeight
) {
when (it.type) {
AudioContentBannerType.EVENT -> {
startActivity(
Intent(requireContext(), EventDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_EVENT, it.eventItem!!)
}
)
}
AudioContentBannerType.CREATOR -> {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it.creatorId!!)
}
)
}
AudioContentBannerType.LINK -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
}
}
}
binding
.rvBanner
.layoutParams = layoutParams
binding.rvBanner.apply {
adapter = bannerAdapter as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(1000)
setInterval(4 * 1000)
}.create()
binding
.rvBanner
.setIndicatorView(binding.indicatorBanner)
.setIndicatorStyle(IndicatorStyle.ROUND_RECT)
.setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
.setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_9970ff)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
}
private fun setupOrderList() {
orderListAdapter = AudioContentMainContentAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvMyStash.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvMyStash.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvMyStash.adapter = orderListAdapter
binding.tvMyStashViewAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentOrderListActivity::class.java))
}
}
private fun setupNewContentTheme() {
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
viewModel.getNewContentOfTheme(theme = it)
}
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContentTheme.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
newContentThemeAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvNewContentTheme.adapter = newContentThemeAdapter
}
private fun setupNewContent() {
binding.ivNewContentAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentNewAllActivity::class.java))
}
newContentAdapter = AudioContentMainContentAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvNewContent.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
newContentAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvNewContent.adapter = newContentAdapter
}
private fun setupContentRankingSortType() {
contentRankingSortAdapter = AudioContentMainNewContentThemeAdapter {
viewModel.getContentRanking(sort = it)
}
binding.rvContentRankingSort.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvContentRankingSort.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
contentRankingSortAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvContentRankingSort.adapter = contentRankingSortAdapter
}
private fun setupContentRanking() {
binding.ivContentRankingAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentRankingAllActivity::class.java))
}
contentRankingAdapter = AudioContentMainRankingAdapter(
width = (screenWidth * 0.66).toInt()
) {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
}
binding.rvContentRanking.layoutManager = GridLayoutManager(
context,
3,
GridLayoutManager.HORIZONTAL,
false
)
binding.rvContentRanking.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
}
})
binding.rvContentRanking.adapter = contentRankingAdapter
}
private fun setupCuration() {
curationAdapter = AudioContentMainCurationAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
},
onClickCurationMore = { curationId, title ->
startActivity(
Intent(requireContext(), AudioContentCurationActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_ID, curationId)
putExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_TITLE, title)
}
)
}
)
binding.rvCuration.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
binding.rvCuration.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 = 40f.dpToPx().toInt()
outRect.bottom = 20f.dpToPx().toInt()
}
curationAdapter.itemCount - 1 -> {
outRect.top = 20f.dpToPx().toInt()
outRect.bottom = 40f.dpToPx().toInt()
}
else -> {
outRect.top = 20f.dpToPx().toInt()
outRect.bottom = 20f.dpToPx().toInt()
}
}
}
})
binding.rvCuration.adapter = curationAdapter
}
@SuppressLint("SetTextI18n")
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.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()
) {
View.GONE
} else {
View.VISIBLE
}
}
viewModel.contentRankingSortListLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
contentRankingSortAdapter.addItems(it)
}
viewModel.contentRankingLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
binding.tvDate.text = "${it.startDate}~${it.endDate}"
contentRankingAdapter.addItems(it.items)
}
}
}

View File

@@ -0,0 +1,35 @@
package kr.co.vividnext.sodalive.audio_content.main
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.ItemAudioContentMainBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainItemViewHolder(
private val binding: ItemAudioContentMainBinding,
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentMainItem) {
binding.ivAudioContentCoverImage.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(2.7f.dpToPx()))
}
binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvAudioContentTitle.text = item.title
binding.tvAudioContentCreatorNickname.text = item.creatorNickname
binding.ivAudioContentCreator.setOnClickListener { onClickCreator(item.creatorId) }
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}

View File

@@ -0,0 +1,52 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainNewContentCreatorBinding
class AudioContentMainNewContentCreatorAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentMainNewContentCreatorAdapter.ViewHolder>() {
private val items = mutableListOf<GetNewContentUploadCreator>()
inner class ViewHolder(
private val binding: ItemAudioContentMainNewContentCreatorBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetNewContentUploadCreator) {
binding.tvNewContentCreator.text = item.creatorNickname
binding.ivNewContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.root.setOnClickListener { onClickItem(item.creatorId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAudioContentMainNewContentCreatorBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetNewContentUploadCreator>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,72 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainNewContentThemeBinding
class AudioContentMainNewContentThemeAdapter(
private val onClickItem: (String) -> Unit
) : RecyclerView.Adapter<AudioContentMainNewContentThemeAdapter.ViewHolder>() {
private val themeList = mutableListOf<String>()
private var selectedTheme = ""
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentMainNewContentThemeBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(theme: String) {
if (
theme == selectedTheme ||
(selectedTheme == "" && theme == "전체") ||
(selectedTheme == "" && theme == "매출")
) {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_9970ff
)
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_9970ff))
} else {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_777777
)
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_777777))
}
binding.tvTheme.text = theme
binding.root.setOnClickListener {
onClickItem(theme)
selectedTheme = theme
notifyDataSetChanged()
}
}
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(themeList: List<String>) {
this.selectedTheme = ""
this.themeList.clear()
this.themeList.addAll(themeList)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemAudioContentMainNewContentThemeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = themeList.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(themeList[position])
}
}

View File

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

View File

@@ -0,0 +1,166 @@
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

@@ -0,0 +1,74 @@
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,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
)
data class GetAudioContentMainItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("title") val title: String,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: String
)
data class GetAudioContentRanking(
@SerializedName("startDate") val startDate: String,
@SerializedName("endDate") val endDate: String,
@SerializedName("items") val items: List<GetAudioContentRankingItem>
)
data class GetAudioContentRankingItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("themeStr") val themeStr: String,
@SerializedName("price") val price: Int,
@SerializedName("duration") val duration: String,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String
)
data class GetAudioContentCurationResponse(
@SerializedName("curationId") val curationId: Long,
@SerializedName("title") val title: String,
@SerializedName("description") val description: String,
@SerializedName("contents") val audioContents: List<GetAudioContentMainItem>
)
data class GetAudioContentBannerResponse(
@SerializedName("type") val type: AudioContentBannerType,
@SerializedName("thumbnailImageUrl") val thumbnailImageUrl: String,
@SerializedName("eventItem") val eventItem: EventItem?,
@SerializedName("creatorId") val creatorId: Long?,
@SerializedName("link") val link: String?
)
enum class AudioContentBannerType {
@SerializedName("EVENT")
EVENT,
@SerializedName("CREATOR")
CREATOR,
@SerializedName("LINK")
LINK
}

View File

@@ -0,0 +1,288 @@
package kr.co.vividnext.sodalive.audio_content.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 androidx.core.view.setPadding
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.databinding.ActivityAudioContentModifyBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBinding>(
ActivityAudioContentModifyBinding::inflate
) {
private val viewModel: AudioContentModifyViewModel 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.ivCover.setPadding(0)
binding.ivCover.background = null
binding.ivCover.load(fileUri) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
viewModel.coverImageUri = 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)
val audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
if (audioContentId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
checkPermissions()
viewModel.getRealPathFromURI = {
RealPathUtil.getRealPath(applicationContext, it)
}
bindData()
viewModel.getAudioContentDetail(audioContentId = audioContentId) { 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) }
}
binding.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) }
binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) }
binding.tvCancel.setOnClickListener { finish() }
binding.tvModify.setOnClickListener {
viewModel.modifyAudioContent { 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.etTitle.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
viewModel.title = it.toString()
}
)
compositeDisposable.add(
binding.etDetail.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
binding.tvNumberOfCharacters.text = "${it.length}"
viewModel.detail = 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.color_eeeeee
)
)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivCommentNo.visibility = View.GONE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llCommentNo.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
} else {
binding.ivCommentNo.visibility = View.VISIBLE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivCommentYes.visibility = View.GONE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llCommentYes
.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734_9970ff)
}
}
viewModel.isAdultShowUiLiveData.observe(this) {
if (it) {
binding.llSetAdult.visibility = View.VISIBLE
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_1f1734
)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.ivAge19.visibility = View.VISIBLE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
} else {
binding.ivAge19.visibility = View.GONE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.ivAgeAll.visibility = View.VISIBLE
binding.llAgeAll.setBackgroundResource(
R.drawable.bg_round_corner_6_7_9970ff
)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
}
}
} else {
binding.llSetAdult.visibility = View.GONE
}
}
viewModel.coverImageLiveData.observe(this) {
binding.ivCover.setPadding(0)
binding.ivCover.background = null
binding.ivCover.load(it) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
}
viewModel.titleLiveData.observe(this) {
binding.etTitle.setText(it)
}
viewModel.detailLiveData.observe(this) {
binding.etDetail.setText(it)
}
}
}

View File

@@ -0,0 +1,212 @@
package kr.co.vividnext.sodalive.audio_content.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.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
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 AudioContentModifyViewModel(
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 val _isAdultLiveData = MutableLiveData(false)
val isAdultLiveData: LiveData<Boolean>
get() = _isAdultLiveData
private val _isAvailableCommentLiveData = MutableLiveData(false)
val isAvailableCommentLiveData: LiveData<Boolean>
get() = _isAvailableCommentLiveData
private val _titleLiveData = MutableLiveData("")
val titleLiveData: LiveData<String>
get() = _titleLiveData
private val _detailLiveData = MutableLiveData("")
val detailLiveData: LiveData<String>
get() = _detailLiveData
private val _coverImageLiveData = MutableLiveData("")
val coverImageLiveData: LiveData<String>
get() = _coverImageLiveData
private val _isAdultShowUiLiveData = MutableLiveData(true)
val isAdultShowUiLiveData: LiveData<Boolean>
get() = _isAdultShowUiLiveData
lateinit var getRealPathFromURI: (Uri) -> String?
var contentId: Long = 0
var title: String? = null
var detail: String? = null
var coverImageUri: Uri? = null
fun setAdult(isAdult: Boolean) {
_isAdultLiveData.postValue(isAdult)
}
fun setAvailableComment(isAvailableComment: Boolean) {
_isAvailableCommentLiveData.postValue(isAvailableComment)
}
fun getAudioContentDetail(audioContentId: Long, onFailure: (() -> Unit)? = null) {
this.contentId = audioContentId
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentDetail(
audioContentId = audioContentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_titleLiveData.value = it.data.title
_detailLiveData.value = it.data.detail
_coverImageLiveData.value = it.data.coverImageUrl
_isAvailableCommentLiveData.value = it.data.isCommentAvailable
_isAdultLiveData.value = it.data.isAdult
_isAdultShowUiLiveData.value = !it.data.isAdult
} 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 modifyAudioContent(onSuccess: () -> Unit) {
if (!_isLoading.value!! && contentId > 0 && validateData()) {
_isLoading.value = true
val request = ModifyAudioContentRequest(
contentId = contentId,
title = title,
detail = detail,
isAdult = _isAdultLiveData.value!!,
isCommentAvailable = _isAvailableCommentLiveData.value!!
)
val requestJson = Gson().toJson(request)
val coverImage = if (coverImageUri != null) {
val file = File(getRealPathFromURI(coverImageUri!!))
MultipartBody.Part.createFormData(
"coverImage",
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.modifyAudioContent(
coverImage = coverImage,
request = requestJson.toRequestBody("text/plain".toMediaType()),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
_toastLiveData.postValue("수정되었습니다.")
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.postValue(false)
},
{
_isLoading.postValue(false)
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
)
)
}
}
private fun validateData(): Boolean {
if (title != null && title!!.isBlank()) {
_toastLiveData.postValue("제목을 입력해 주세요.")
return false
}
if (detail != null && (detail!!.isBlank() || detail!!.length < 5)) {
_toastLiveData.postValue("내용을 5자 이상 입력해 주세요.")
return false
}
return true
}
}

View File

@@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.audio_content.modify
import com.google.gson.annotations.SerializedName
data class ModifyAudioContentRequest(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String?,
@SerializedName("detail") val detail: String?,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean
)

View File

@@ -0,0 +1,93 @@
package kr.co.vividnext.sodalive.audio_content.order
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.DialogAudioContentOrderConfirmBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kotlin.math.ceil
class AudioContentOrderConfirmDialog(
activity: Activity,
layoutInflater: LayoutInflater,
title: String,
theme: String,
coverImageUrl: String,
profileImageUrl: String,
nickname: String,
duration: String,
isOnlyRental: Boolean,
orderType: OrderType,
price: Int,
confirmButtonClick: () -> Unit,
) {
private val alertDialog: AlertDialog
val dialogView = DialogAudioContentOrderConfirmBinding.inflate(layoutInflater)
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialogView.tvTitle.text = title
dialogView.tvTheme.text = theme
dialogView.tvProfileNickname.text = nickname
dialogView.ivCover.load(coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4f))
}
dialogView.ivProfile.load(profileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
dialogView.tvDuration.text = duration
dialogView.tvPrice.text = if (orderType == OrderType.RENTAL && !isOnlyRental) {
"${ceil(price * 0.6).toInt()}"
} else {
"$price"
}
dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
"콘텐츠를 대여하시겠습니까?\n아래 캔이 차감됩니다."
} else {
"콘텐츠를 소장하시겠습니까?\n아래 캔이 차감됩니다."
}
dialogView.tvCancel.setOnClickListener {
alertDialog.dismiss()
}
dialogView.tvConfirm.setOnClickListener {
alertDialog.dismiss()
confirmButtonClick()
}
}
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,51 @@
package kr.co.vividnext.sodalive.audio_content.order
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentOrderBinding
import kotlin.math.ceil
class AudioContentOrderFragment(
private val price: Int,
private val isOnlyRental: Boolean,
private val onClickRental: () -> Unit,
private val onClickKeep: () -> Unit
) : BottomSheetDialogFragment() {
private lateinit var binding: FragmentAudioContentOrderBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentAudioContentOrderBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (isOnlyRental) {
binding.tvRental.text = "$price"
binding.rlKeep.visibility = View.GONE
} else {
binding.tvKeep.text = "$price"
binding.tvRental.text = "${ceil(price * 0.6).toInt()}"
binding.rlKeep.visibility = View.VISIBLE
binding.llKeep.setOnClickListener {
onClickKeep()
dismiss()
}
}
binding.llRental.setOnClickListener {
onClickRental()
dismiss()
}
}
}

View File

@@ -0,0 +1,131 @@
package kr.co.vividnext.sodalive.audio_content.order
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentOrderListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentOrderListActivity : BaseActivity<ActivityAudioContentOrderListBinding>(
ActivityAudioContentOrderListBinding::inflate
) {
private val viewModel: AudioContentOrderListViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentOrderListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.getAudioContentOrderList { finish() }
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "구매목록"
binding.toolbar.tvBack.setOnClickListener { finish() }
adapter = AudioContentOrderListAdapter {
startActivity(
Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply { putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) }
)
}
binding.rvOrderList.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
binding.rvOrderList.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
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()
}
}
}
})
binding.rvOrderList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getAudioContentOrderList {}
}
}
})
binding.rvOrderList.adapter = adapter
}
@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.orderList.observe(this) {
if (viewModel.page == 2) {
adapter.items.clear()
}
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
viewModel.totalCount.observe(this) {
binding.tvTotalCount.text = "$it"
}
}
}

View File

@@ -0,0 +1,62 @@
package kr.co.vividnext.sodalive.audio_content.order
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioContentOrderListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentOrderListAdapter(
private val onItemClick: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentOrderListAdapter.ViewHolder>() {
var items = mutableSetOf<GetAudioContentOrderListItem>()
inner class ViewHolder(
private val binding: ItemAudioContentOrderListBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentOrderListItem) {
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
binding.tvTitle.text = item.title
binding.tvTheme.text = item.themeStr
binding.tvDuration.text = item.duration
binding.tvCreatorNickname.text = item.creatorNickname
binding.tvLikeCount.text = item.likeCount.moneyFormat()
binding.tvCommentCount.text = item.commentCount.moneyFormat()
if (item.orderType == OrderType.RENTAL) {
binding.tvRental.visibility = View.VISIBLE
binding.tvPurchased.visibility = View.GONE
} else {
binding.tvPurchased.visibility = View.VISIBLE
binding.tvRental.visibility = View.GONE
}
binding.root.setOnClickListener { onItemClick(item.contentId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAudioContentOrderListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items.toList()[position])
}
override fun getItemCount() = items.size
}

View File

@@ -0,0 +1,84 @@
package kr.co.vividnext.sodalive.audio_content.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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentOrderListViewModel(
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 _orderList = MutableLiveData<List<GetAudioContentOrderListItem>>()
val orderList: LiveData<List<GetAudioContentOrderListItem>>
get() = _orderList
private var _totalCount = MutableLiveData<Int>()
val totalCount: LiveData<Int>
get() = _totalCount
private var isLast = false
var page = 1
private val size = 10
fun getAudioContentOrderList(onFailure: (() -> Unit)? = null) {
if (_isLoading.value == false) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentOrderList(
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_totalCount.value = it.data.totalCount
if (it.data.items.isNotEmpty()) {
page += 1
_orderList.postValue(it.data.items)
} else {
isLast = true
}
} else {
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,21 @@
package kr.co.vividnext.sodalive.audio_content.order
import com.google.gson.annotations.SerializedName
data class GetAudioContentOrderListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentOrderListItem>
)
data class GetAudioContentOrderListItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("title") val title: String,
@SerializedName("themeStr") val themeStr: String,
@SerializedName("duration") val duration: String?,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("orderType") val orderType: OrderType,
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentCount") val commentCount: Int,
)

View File

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.order
import com.google.gson.annotations.SerializedName
data class OrderRequest(
@SerializedName("contentId") val contentId: Long,
@SerializedName("orderType") val orderType: OrderType,
@SerializedName("container") val container: String
)

View File

@@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.audio_content.order
import com.google.gson.annotations.SerializedName
enum class OrderType {
@SerializedName("RENTAL")
RENTAL,
@SerializedName("KEEP")
KEEP
}

View File

@@ -0,0 +1,532 @@
package kr.co.vividnext.sodalive.audio_content.upload
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.OpenableColumns
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.CircleCropTransformation
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.audio_content.upload.theme.AudioContentThemeFragment
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.ActivityAudioContentUploadBinding
import kr.co.vividnext.sodalive.dialog.LiveDialog
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
import java.io.File
class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBinding>(
ActivityAudioContentUploadBinding::inflate
) {
private val viewModel: AudioContentUploadViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private val themeFragment: AudioContentThemeFragment by lazy {
AudioContentThemeFragment(
getSelectedTheme = { viewModel.theme },
onItemClick = {
binding.ivTheme.load(it.image) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
binding.ivTheme.visibility = View.VISIBLE
}
binding.tvTheme.text = it.theme
viewModel.theme = it
}
)
}
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.ivCover.background = null
binding.ivCover.load(fileUri) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
viewModel.coverImageUri = 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()
}
}
private val selectAudioActivityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = data?.data
if (fileUri != null) {
binding.tvSelectContent.text = getFileName(fileUri)
viewModel.contentUri = fileUri
} else {
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
binding.tvSelectContent.text = "파일 선택"
viewModel.contentUri = null
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.llTheme.setOnClickListener {
if (themeFragment.isAdded) return@setOnClickListener
themeFragment.show(supportFragmentManager, themeFragment.tag)
}
binding.ivPhotoPicker.setOnClickListener {
ImagePicker.with(this)
.crop()
.galleryOnly()
.galleryMimeTypes( // Exclude gif images
mimeTypes = arrayOf(
"image/png",
"image/jpg",
"image/jpeg"
)
)
.createIntent { imageResult.launch(it) }
}
binding.tvSelectContent.setOnClickListener {
val intent = Intent().apply {
type = "audio/*"
action = Intent.ACTION_GET_CONTENT
addCategory(Intent.CATEGORY_OPENABLE)
}
selectAudioActivityResultLauncher.launch(
Intent.createChooser(
intent,
"Select Audio"
)
)
}
if (SharedPreferenceManager.isAuth) {
binding.llSetAdult.visibility = View.VISIBLE
} else {
binding.llSetAdult.visibility = View.GONE
}
binding.llPricePaid.setOnClickListener { viewModel.setPriceFree(false) }
binding.llPriceFree.setOnClickListener { viewModel.setPriceFree(true) }
binding.llRentalAndKeep.setOnClickListener { viewModel.setIsOnlyRental(false) }
binding.llOnlyRental.setOnClickListener { viewModel.setIsOnlyRental(true) }
binding.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) }
binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) }
binding.tvCancel.setOnClickListener { finish() }
binding.tvUpload.setOnClickListener {
viewModel.uploadAudioContent {
LiveDialog(
activity = this,
layoutInflater = layoutInflater,
title = "콘텐츠 업로드",
desc = "등록한 콘텐츠가 업로드 중입니다.\n" +
"콘텐츠 등록이 완료되면 알림을 보내드립니다.\n" +
"이 페이지를 나가도 콘텐츠는 자동으로 등록됩니다.",
confirmButtonTitle = "확인",
confirmButtonClick = { finish() },
).show(screenWidth)
}
}
}
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.etTitle.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
viewModel.title = it.toString()
}
)
compositeDisposable.add(
binding.etDetail.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
binding.tvNumberOfCharacters.text = "${it.length}"
viewModel.detail = it.toString()
}
)
compositeDisposable.add(
binding.etTag.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
viewModel.tags = it.toString()
}
)
compositeDisposable.add(
binding.etSetPrice.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
val price = it.toString().toIntOrNull()
if (price != null) {
viewModel.price = price.toInt()
} else {
viewModel.price = 0
if (it.isNotBlank()) {
binding.etSetPrice.setText(it.substring(0, it.length - 1))
binding.etSetPrice.setSelection(it.length - 1)
}
}
}
)
compositeDisposable.add(
binding.etPreviewStartTime.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.isNotBlank()) {
viewModel.previewStartTime = it.toString()
} else {
viewModel.previewStartTime = null
}
}
)
compositeDisposable.add(
binding.etPreviewEndTime.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.isNotBlank()) {
viewModel.previewEndTime = it.toString()
} else {
viewModel.previewEndTime = null
}
}
)
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "콘텐츠를 업로드 하는 중입니다.")
} else {
loadingDialog.dismiss()
}
}
viewModel.isPriceFreeLiveData.observe(this) {
if (it) {
checkPriceFree()
} else {
checkPricePaid()
}
}
viewModel.isOnlyRentalLiveData.observe(this) {
if (it) {
checkOnlyRental()
} else {
checkRentalAndKeep()
}
}
viewModel.isAvailableCommentLiveData.observe(this) {
if (it) {
binding.ivCommentYes.visibility = View.VISIBLE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivCommentNo.visibility = View.GONE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llCommentNo.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
} else {
binding.ivCommentNo.visibility = View.VISIBLE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivCommentYes.visibility = View.GONE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llCommentYes
.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734_9970ff)
}
}
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_1f1734)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.ivAge19.visibility = View.VISIBLE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
} else {
binding.ivAge19.visibility = View.GONE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.ivAgeAll.visibility = View.VISIBLE
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
}
}
}
}
private fun checkPriceFree() {
viewModel.price = 0
binding.etSetPrice.setText("0")
binding.llSetPrice.visibility = View.GONE
binding.llConfigKeep.visibility = View.GONE
binding.tvTitleConfigKeep.visibility = View.GONE
binding.ivPriceFree.visibility = View.VISIBLE
binding.tvPriceFree.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llPriceFree.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivPricePaid.visibility = View.GONE
binding.tvPricePaid.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llPricePaid.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
binding.llConfigPreviewTime.visibility = View.GONE
}
private fun checkPricePaid() {
binding.llSetPrice.visibility = View.VISIBLE
binding.llConfigKeep.visibility = View.VISIBLE
binding.tvTitleConfigKeep.visibility = View.VISIBLE
binding.ivPricePaid.visibility = View.VISIBLE
binding.tvPricePaid.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llPricePaid.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivPriceFree.visibility = View.GONE
binding.tvPriceFree.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llPriceFree.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
binding.llConfigPreviewTime.visibility = View.VISIBLE
}
private fun checkRentalAndKeep() {
binding.tvPriceTitle.text = "소장 가격"
binding.ivRentalAndKeep.visibility = View.VISIBLE
binding.tvRentalAndKeep.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llRentalAndKeep.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivOnlyRental.visibility = View.GONE
binding.tvOnlyRental.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llOnlyRental.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
}
private fun checkOnlyRental() {
binding.tvPriceTitle.text = "대여 가격"
binding.ivOnlyRental.visibility = View.VISIBLE
binding.tvOnlyRental.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llOnlyRental.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivRentalAndKeep.visibility = View.GONE
binding.tvRentalAndKeep.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llRentalAndKeep.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
}
private fun getFileName(uri: Uri): String? {
val scheme = uri.scheme
var fileName: String? = null
if (scheme == "content") {
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameIndex != -1 && cursor.moveToFirst()) {
fileName = cursor.getString(nameIndex)
}
}
} else if (scheme == "file") {
val file = File(uri.path ?: "")
fileName = file.name
}
return fileName
}
}

View File

@@ -0,0 +1,310 @@
package kr.co.vividnext.sodalive.audio_content.upload
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.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
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
import java.text.SimpleDateFormat
import java.util.Locale
class AudioContentUploadViewModel(
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 val _isAdultLiveData = MutableLiveData(false)
val isAdultLiveData: LiveData<Boolean>
get() = _isAdultLiveData
private val _isOnlyRentalLiveData = MutableLiveData(false)
val isOnlyRentalLiveData: LiveData<Boolean>
get() = _isOnlyRentalLiveData
private val _isAvailableCommentLiveData = MutableLiveData(true)
val isAvailableCommentLiveData: LiveData<Boolean>
get() = _isAvailableCommentLiveData
private val _isPriceFreeLiveData = MutableLiveData(true)
val isPriceFreeLiveData: LiveData<Boolean>
get() = _isPriceFreeLiveData
lateinit var getRealPathFromURI: (Uri) -> String?
var title = ""
var detail = ""
var tags = ""
var price = 0
var theme: GetAudioContentThemeResponse? = null
var coverImageUri: Uri? = null
var contentUri: Uri? = null
var previewStartTime: String? = null
var previewEndTime: String? = null
fun setAdult(isAdult: Boolean) {
_isAdultLiveData.postValue(isAdult)
}
fun setAvailableComment(isAvailableComment: Boolean) {
_isAvailableCommentLiveData.postValue(isAvailableComment)
}
fun setPriceFree(isPriceFree: Boolean) {
_isPriceFreeLiveData.postValue(isPriceFree)
if (isPriceFree) {
_isOnlyRentalLiveData.postValue(false)
}
}
fun setIsOnlyRental(isOnlyRental: Boolean) {
_isOnlyRentalLiveData.postValue(isOnlyRental)
}
fun uploadAudioContent(onSuccess: () -> Unit) {
if (!_isLoading.value!! && validateData()) {
_isLoading.postValue(true)
val request = CreateAudioContentRequest(
title = title,
detail = detail,
tags = tags,
price = price,
themeId = theme!!.id,
isAdult = _isAdultLiveData.value!!,
isOnlyRental = _isOnlyRentalLiveData.value!!,
isCommentAvailable = _isAvailableCommentLiveData.value!!,
previewStartTime = previewStartTime,
previewEndTime = previewEndTime
)
val requestJson = Gson().toJson(request)
val coverImage = if (coverImageUri != null) {
val file = File(getRealPathFromURI(coverImageUri!!))
MultipartBody.Part.createFormData(
"coverImage",
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
}
val contentFile = if (contentUri != null) {
val file = File(getRealPathFromURI(contentUri!!))
MultipartBody.Part.createFormData(
"contentFile",
file.name,
body = object : RequestBody() {
override fun contentType(): MediaType {
return "audio/*".toMediaType()
}
override fun writeTo(sink: BufferedSink) {
file.inputStream().use { inputStream ->
val buffer = ByteArray(512)
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
}
if (coverImage == null) {
_toastLiveData.postValue("커버이미지를 선택해 주세요.")
return
}
if (contentFile == null) {
_toastLiveData.postValue("오디오 콘텐츠를 선택해 주세요.")
return
}
compositeDisposable.add(
repository.uploadAudioContent(
coverImage = coverImage,
contentFile = contentFile,
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 (title.isBlank()) {
_toastLiveData.postValue("제목을 입력해 주세요.")
return false
}
if (detail.isBlank() || detail.length < 5) {
_toastLiveData.postValue("내용을 5자 이상 입력해 주세요.")
return false
}
if (theme == null) {
_toastLiveData.postValue("테마를 선택해 주세요.")
return false
}
if (coverImageUri == null) {
_toastLiveData.postValue("커버이미지를 선택해 주세요.")
return false
}
if (previewStartTime != null && previewEndTime != null) {
val startTimeArray = previewStartTime!!.split(":")
if (startTimeArray.size != 3) {
_toastLiveData.postValue("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
return false
}
for (time in startTimeArray) {
if (time.length != 2) {
_toastLiveData.postValue("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
return false
}
}
val endTimeArray = previewEndTime!!.split(":")
if (endTimeArray.size != 3) {
_toastLiveData.postValue("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
return false
}
for (time in endTimeArray) {
if (time.length != 2) {
_toastLiveData.postValue("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
return false
}
}
val timeDifference = timeDifference(previewStartTime!!, previewEndTime!!)
if (timeDifference < 30000) {
_toastLiveData.postValue(
"미리 듣기의 최소 시간은 30초 입니다."
)
return false
}
} else {
if (previewStartTime != null || previewEndTime != null) {
_toastLiveData.postValue(
"미리 듣기 시작 시간과 종료 시간 둘 다 입력을 하거나 둘 다 입력 하지 않아야 합니다."
)
return false
}
}
if (contentUri == null) {
_toastLiveData.postValue("오디오 콘텐츠를 선택해 주세요.")
return false
}
if (!isPriceFreeLiveData.value!! && price < 5) {
_toastLiveData.postValue("콘텐츠의 최소금액은 5캔 입니다.")
return false
}
return true
}
private fun timeDifference(startTime: String, endTime: String): Long {
try {
// Define a date format for parsing the times
val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.KOREAN)
// Parse the input times into Date objects
val date1 = dateFormat.parse(startTime)
val date2 = dateFormat.parse(endTime)
// Check if either date is null
if (date1 == null || date2 == null) {
return 0
}
// Calculate the absolute time difference in milliseconds
// Check if the time difference is greater than 30 seconds (30000 milliseconds)
return date2.time - date1.time
} catch (e: Exception) {
// Handle invalid time formats or parsing errors
return 0
}
}
}

View File

@@ -0,0 +1,16 @@
package kr.co.vividnext.sodalive.audio_content.upload
import com.google.gson.annotations.SerializedName
data class CreateAudioContentRequest(
@SerializedName("title") val title: String,
@SerializedName("detail") val detail: String,
@SerializedName("tags") val tags: String,
@SerializedName("price") val price: Int,
@SerializedName("themeId") val themeId: Long,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isOnlyRental") val isOnlyRental: Boolean,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean,
@SerializedName("previewStartTime") val previewStartTime: String? = null,
@SerializedName("previewEndTime") val previewEndTime: String? = null
)

View File

@@ -0,0 +1,64 @@
package kr.co.vividnext.sodalive.audio_content.upload.theme
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioContentThemeBinding
class AudioContentThemeAdapter(
private val selectedTheme: GetAudioContentThemeResponse?,
private val onItemClick: (GetAudioContentThemeResponse) -> Unit
) : RecyclerView.Adapter<AudioContentThemeAdapter.ViewHolder>() {
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentThemeBinding
) : RecyclerView.ViewHolder(binding.root) {
private var isChecked = false
fun bind(item: GetAudioContentThemeResponse) {
if (selectedTheme == item) {
binding.ivThemeChecked.visibility = View.VISIBLE
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_9970ff))
isChecked = true
} else {
binding.ivThemeChecked.visibility = View.GONE
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_bbbbbb))
isChecked = false
}
binding.ivTheme.load(item.image) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvTheme.text = item.theme
binding.root.setOnClickListener { onItemClick(item) }
}
}
val items = mutableSetOf<GetAudioContentThemeResponse>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemAudioContentThemeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items.toList()[position])
}
override fun getItemCount() = items.size
}

View File

@@ -0,0 +1,102 @@
package kr.co.vividnext.sodalive.audio_content.upload.theme
import android.annotation.SuppressLint
import android.app.Dialog
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentThemeFragment(
private val getSelectedTheme: () -> GetAudioContentThemeResponse?,
private val onItemClick: (GetAudioContentThemeResponse) -> Unit
) : BottomSheetDialogFragment() {
private val viewModel: AudioContentThemeViewModel by inject()
private lateinit var adapter: AudioContentThemeAdapter
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.setOnShowListener {
val d = it as BottomSheetDialog
val bottomSheet = d.findViewById<FrameLayout>(
com.google.android.material.R.id.design_bottom_sheet
)
if (bottomSheet != null) {
BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_EXPANDED
}
}
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_audio_content_theme, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<ImageView>(R.id.iv_close).setOnClickListener {
dialog?.dismiss()
}
setupAdapter(view)
bindData()
viewModel.getThemes()
}
private fun setupAdapter(view: View) {
val recyclerView = view.findViewById<RecyclerView>(R.id.rv_themes)
adapter = AudioContentThemeAdapter(getSelectedTheme()) {
onItemClick(it)
dialog?.dismiss()
}
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = GridLayoutManager(requireContext(), 4)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
}
})
recyclerView.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
}
viewModel.themeLiveData.observe(viewLifecycleOwner) {
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
}
}

View File

@@ -0,0 +1,47 @@
package kr.co.vividnext.sodalive.audio_content.upload.theme
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 AudioContentThemeViewModel(private val repository: AudioContentRepository) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _themeLiveData = MutableLiveData<List<GetAudioContentThemeResponse>>()
val themeLiveData: LiveData<List<GetAudioContentThemeResponse>>
get() = _themeLiveData
fun getThemes() {
compositeDisposable.add(
repository.getAudioContentThemeList(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_themeLiveData.postValue(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

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.upload.theme
import com.google.gson.annotations.SerializedName
data class GetAudioContentThemeResponse(
@SerializedName("id") val id: Long,
@SerializedName("theme") val theme: String,
@SerializedName("image") val image: String
)

View File

@@ -0,0 +1,74 @@
package kr.co.vividnext.sodalive.base
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import kr.co.vividnext.sodalive.databinding.DialogSodaBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
open class SodaDialog(
activity: Activity,
layoutInflater: LayoutInflater,
title: String,
desc: String,
confirmButtonTitle: String,
confirmButtonClick: () -> Unit,
cancelButtonTitle: String = "",
cancelButtonClick: (() -> Unit)? = null,
) {
private val alertDialog: AlertDialog
val dialogView = DialogSodaBinding.inflate(layoutInflater)
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialogView.tvTitle.text = title
dialogView.tvDesc.text = desc
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) {
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,9 @@
package kr.co.vividnext.sodalive.common
import retrofit2.Retrofit
class ApiBuilder {
fun <T> build(retrofit: Retrofit, service: Class<T>): T {
return retrofit.create(service)
}
}

View File

@@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.common
import com.google.gson.annotations.SerializedName
data class ApiResponse<T>(
@SerializedName("success") val success: Boolean,
@SerializedName("data") val data: T? = null,
@SerializedName("message") val message: String? = null,
@SerializedName("errorProperty") val errorProperty: String? = null
)

View File

@@ -0,0 +1,59 @@
package kr.co.vividnext.sodalive.common
object Constants {
const val PREF_CAN = "pref_can"
const val PREF_TOKEN = "pref_token"
const val PREF_EMAIL = "pref_email"
const val PREF_USER_ID = "pref_user_id"
const val PREF_IS_ADULT = "pref_is_adult"
const val PREF_NICKNAME = "pref_nickname"
const val PREF_USER_ROLE = "pref_user_role"
const val PREF_NO_CHAT_ROOM = "pref_no_chat"
const val PREF_PUSH_TOKEN = "pref_push_token"
const val PREF_PROFILE_IMAGE = "pref_profile_image"
const val PREF_IS_CONTENT_PLAY_LOOP = "pref_is_content_play_loop"
const val PREF_IS_FOLLOWED_CREATOR_LIVE = "pref_is_followed_creator_live"
const val PREF_NOT_SHOWING_EVENT_POPUP_ID = "pref_not_showing_event_popup_id"
const val PREF_IS_VIEWED_ON_BOARDING_TUTORIAL = "pref_is_viewed_on_boarding_tutorial"
const val EXTRA_CAN = "extra_can"
const val EXTRA_DATA = "extra_data"
const val EXTRA_TERMS = "extra_terms"
const val EXTRA_EVENT = "extra_event"
const val EXTRA_NOTICE = "extra_notice"
const val EXTRA_ROOM_ID = "extra_room_id"
const val EXTRA_USER_ID = "extra_user_id"
const val EXTRA_NICKNAME = "extra_nickname"
const val EXTRA_MESSAGE_ID = "extra_message_id"
const val EXTRA_ROOM_DETAIL = "extra_room_detail"
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_GO_TO_PREV_PAGE = "extra_go_to_prev_page"
const val EXTRA_SELECT_RECIPIENT = "extra_select_recipient"
const val EXTRA_ROOM_CHANNEL_NAME = "extra_room_channel_name"
const val EXTRA_LIVE_RESERVATION_RESPONSE = "extra_live_reservation_response"
const val EXTRA_AUDIO_CONTENT_ID = "audio_content_id"
const val EXTRA_AUDIO_CONTENT_URL = "audio_content_url"
const val EXTRA_AUDIO_CONTENT_TITLE = "audio_content_title"
const val EXTRA_AUDIO_CONTENT_FREE = "audio_content_is_free"
const val EXTRA_AUDIO_CONTENT_PREVIEW = "audio_content_is_preview"
const val EXTRA_AUDIO_CONTENT_PLAYING = "audio_content_is_playing"
const val EXTRA_AUDIO_CONTENT_SHOWING = "audio_content_is_showing"
const val EXTRA_AUDIO_CONTENT_CHANGE_UI = "audio_content_change_ui"
const val EXTRA_AUDIO_CONTENT_PROGRESS = "audio_content_progress"
const val EXTRA_AUDIO_CONTENT_DURATION = "audio_content_duration"
const val EXTRA_AUDIO_CONTENT_COMMENT = "audio_content_comment"
const val EXTRA_AUDIO_CONTENT_LOADING = "audio_content_loading"
const val EXTRA_AUDIO_CONTENT_CREATOR_ID = "audio_content_creator_id"
const val EXTRA_AUDIO_CONTENT_CURATION_ID = "extra_audio_content_curation_id"
const val EXTRA_AUDIO_CONTENT_CURATION_TITLE = "extra_audio_content_curation_title"
const val EXTRA_AUDIO_CONTENT_NEXT_ACTION = "audio_content_next_action"
const val EXTRA_AUDIO_CONTENT_ALERT_PREVIEW = "audio_content_alert_preview"
const val EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL = "audio_content_cover_image_url"
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"
}

View File

@@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.common
import android.graphics.Typeface
import android.text.TextPaint
import android.text.style.MetricAffectingSpan
class CustomTypefaceSpan(private val typeface: Typeface?) : MetricAffectingSpan() {
override fun updateDrawState(tp: TextPaint) {
tp.typeface = typeface
}
override fun updateMeasureState(textPaint: TextPaint) {
textPaint.typeface = typeface
}
}

View File

@@ -0,0 +1,43 @@
package kr.co.vividnext.sodalive.common
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import kr.co.vividnext.sodalive.databinding.DialogLoadingBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class LoadingDialog(
activity: Activity,
layoutInflater: LayoutInflater
) {
private val alertDialog: AlertDialog
private val dialogView = DialogLoadingBinding.inflate(layoutInflater)
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
}
fun show(width: Int, message: String = "") {
alertDialog.show()
dialogView.tvLoading.text = message
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
}
fun dismiss() {
alertDialog.dismiss()
}
}

View File

@@ -0,0 +1,14 @@
package kr.co.vividnext.sodalive.common
import android.content.Context
import io.objectbox.BoxStore
import kr.co.vividnext.sodalive.audio_content.MyObjectBox
import kr.co.vividnext.sodalive.audio_content.PlaybackTracking
class ObjectBox(context: Context) {
private var store: BoxStore = MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
val playbackTrackingBox = store.boxFor(PlaybackTracking::class.java)
}

View File

@@ -0,0 +1,171 @@
package kr.co.vividnext.sodalive.common
import android.annotation.SuppressLint
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.text.TextUtils
object RealPathUtil {
fun getRealPath(context: Context, fileUri: Uri): String? {
return getRealPathFromURIAPI19(context, fileUri) // SDK > 19 (Android 4.4) and up
}
/**
* Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and
* other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @author Niks
*/
@SuppressLint("NewApi")
fun getRealPathFromURIAPI19(context: Context, uri: Uri): String? {
// DocumentProvider
if (DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val type = split[0]
if ("primary".equals(type, ignoreCase = true)) {
return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
}
} else if (isDownloadsDocument(uri)) {
var cursor: Cursor? = null
try {
cursor = context.contentResolver.query(
uri,
arrayOf(MediaStore.MediaColumns.DISPLAY_NAME),
null,
null,
null
)
cursor!!.moveToNext()
val fileName = cursor.getString(0)
val path = Environment.getExternalStorageDirectory()
.toString() + "/Download/" + fileName
if (!TextUtils.isEmpty(path)) {
return path
}
} finally {
cursor?.close()
}
val id = DocumentsContract.getDocumentId(uri)
if (id.startsWith("raw:")) {
return id.replaceFirst("raw:".toRegex(), "")
}
val contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads"),
java.lang.Long.valueOf(id)
)
return getDataColumn(context, contentUri, null, null)
} else if (isMediaDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
"video" -> contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
"audio" -> contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
return getDataColumn(context, contentUri, selection, selectionArgs)
} // MediaProvider
// DownloadsProvider
} else if ("content".equals(uri.scheme!!, ignoreCase = true)) {
// Return the remote address
return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(
context,
uri,
null,
null
)
} else if ("file".equals(uri.scheme!!, ignoreCase = true)) {
return uri.path
} // File
// MediaStore (and general)
return null
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
* @author Niks
*/
private fun getDataColumn(
context: Context,
uri: Uri?,
selection: String?,
selectionArgs: Array<String>?
): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)
try {
cursor =
context.contentResolver.query(uri!!, projection, selection, selectionArgs, null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
private fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
private fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
}

View File

@@ -0,0 +1,141 @@
package kr.co.vividnext.sodalive.common
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kr.co.vividnext.sodalive.settings.notification.MemberRole
object SharedPreferenceManager {
private lateinit var sharedPreferences: SharedPreferences
fun init(context: Context) {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
}
fun clear() {
sharedPreferences.edit { it.clear() }
}
private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
val editor = this.edit()
operation(editor)
editor.apply()
}
private operator fun SharedPreferences.set(key: String, value: Any?) {
when (value) {
is String? -> edit { it.putString(key, value) }
is Int -> edit { it.putInt(key, value) }
is Boolean -> edit { it.putBoolean(key, value) }
is Float -> edit { it.putFloat(key, value) }
is Long -> edit { it.putLong(key, value) }
else -> throw UnsupportedOperationException("Error")
}
}
@Suppress("UNCHECKED_CAST")
private operator fun <T> SharedPreferences.get(key: String, defaultValue: T? = null): T {
return when (defaultValue) {
is String, null -> getString(key, defaultValue as? String) as T
is Int -> getInt(key, defaultValue as? Int ?: -1) as T
is Boolean -> getBoolean(key, defaultValue as? Boolean ?: false) as T
is Float -> getFloat(key, defaultValue as? Float ?: -1f) as T
is Long -> getLong(key, defaultValue as? Long ?: -1) as T
else -> throw UnsupportedOperationException("Error")
}
}
var token: String
get() = sharedPreferences[Constants.PREF_TOKEN, ""]
set(value) {
sharedPreferences[Constants.PREF_TOKEN] = value
}
var userId: Long
get() = sharedPreferences[Constants.PREF_USER_ID, 0]
set(value) {
sharedPreferences[Constants.PREF_USER_ID] = value
}
var nickname: String
get() = sharedPreferences[Constants.PREF_NICKNAME, ""]
set(value) {
sharedPreferences[Constants.PREF_NICKNAME] = value
}
var email: String
get() = sharedPreferences[Constants.PREF_EMAIL, ""]
set(value) {
sharedPreferences[Constants.PREF_EMAIL] = value
}
var profileImage: String
get() = sharedPreferences[Constants.PREF_PROFILE_IMAGE, ""]
set(value) {
sharedPreferences[Constants.PREF_PROFILE_IMAGE] = value
}
var can: Int
get() = sharedPreferences[Constants.PREF_CAN, 0]
set(value) {
sharedPreferences[Constants.PREF_CAN] = value
}
var role: String
get() = sharedPreferences[Constants.PREF_USER_ROLE, MemberRole.USER.name]
set(value) {
sharedPreferences[Constants.PREF_USER_ROLE] = value
}
var isAuth: Boolean
get() = sharedPreferences[Constants.PREF_IS_ADULT, false]
set(value) {
sharedPreferences[Constants.PREF_IS_ADULT] = value
}
var pushToken: String
get() = sharedPreferences[Constants.PREF_PUSH_TOKEN, ""]
set(value) {
sharedPreferences[Constants.PREF_PUSH_TOKEN] = value
}
var isFollowedCreatorLive: Boolean
get() = sharedPreferences[Constants.PREF_IS_FOLLOWED_CREATOR_LIVE, false]
set(value) {
sharedPreferences[Constants.PREF_IS_FOLLOWED_CREATOR_LIVE] = value
}
var isContentPlayLoop: Boolean
get() = sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP, false]
set(value) {
sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP] = value
}
var notShowingEventPopupId: Long
get() = sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID, 0]
set(value) {
sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID] = value
}
var isViewedOnboardingTutorial: Boolean
get() = sharedPreferences[Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL, false]
set(value) {
sharedPreferences[Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL] = value
}
var noChatRoomList: List<Long>
get() {
val list = sharedPreferences[Constants.PREF_NO_CHAT_ROOM, ""]
val gson = Gson()
val listType = object : TypeToken<List<Long>>() {}.type
val myList = gson.fromJson<List<Long>>(list, listType)
return myList ?: emptyList()
}
set(value) {
val gson = Gson()
val listJson = gson.toJson(value)
sharedPreferences[Constants.PREF_NO_CHAT_ROOM] = listJson
}
}

View File

@@ -0,0 +1,90 @@
package kr.co.vividnext.sodalive.common
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
import org.koin.android.ext.android.inject
class SodaLiveService : Service() {
private val liveViewModel: LiveViewModel by inject()
var roomId: Long = 0
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val content = intent?.getStringExtra("content") ?: "라이브 진행중"
roomId = intent?.getLongExtra("roomId", 0) ?: 0L
updateNotification(content)
return START_STICKY
}
private fun updateNotification(content: String) {
startForeground(Constants.LIVE_SERVICE_NOTIFICATION_ID, createNotification(content))
}
private fun createNotification(content: String): Notification {
val notificationChannelId = "soda_live_service_foreground_service_channel"
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(
notificationChannelId,
getString(R.string.app_name),
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(channel)
}
val intent = Intent(this, LiveRoomActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
)
val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(getString(R.string.app_name))
.setContentText(content)
.setOngoing(true)
.setSilent(true)
.setContentIntent(pendingIntent)
return notificationBuilder.build()
}
override fun onDestroy() {
liveViewModel.quitRoom(roomId) { }
super.onDestroy()
}
override fun onTaskRemoved(rootIntent: Intent?) {
stopSelf()
}
companion object {
fun stopService(context: Context) {
val intent = Intent(context, SodaLiveService::class.java)
context.stopService(intent)
}
}
}

View File

@@ -0,0 +1,12 @@
package kr.co.vividnext.sodalive.common
object Utils {
fun convertDurationToString(duration: Int): String {
val durationSeconds = duration / 1000
val hours = (durationSeconds / 3600)
val minutes = ((durationSeconds % 3600) / 60)
val seconds = (durationSeconds % 60)
return "%02d:%02d:%02d".format(hours, minutes, seconds)
}
}

View File

@@ -0,0 +1,237 @@
package kr.co.vividnext.sodalive.di
import android.content.Context
import com.google.gson.GsonBuilder
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
import kr.co.vividnext.sodalive.audio_content.PlaybackTrackingRepository
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllViewModel
import kr.co.vividnext.sodalive.audio_content.all.AudioContentRankingAllViewModel
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentListViewModel
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentReplyViewModel
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentRepository
import kr.co.vividnext.sodalive.audio_content.curation.AudioContentCurationViewModel
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailViewModel
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainViewModel
import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyViewModel
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListViewModel
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadViewModel
import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeViewModel
import kr.co.vividnext.sodalive.common.ApiBuilder
import kr.co.vividnext.sodalive.common.ObjectBox
import kr.co.vividnext.sodalive.explorer.ExplorerApi
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.explorer.ExplorerViewModel
import kr.co.vividnext.sodalive.explorer.profile.UserProfileViewModel
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAllViewModel
import kr.co.vividnext.sodalive.explorer.profile.fantalk.UserProfileFantalkAllViewModel
import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListViewModel
import kr.co.vividnext.sodalive.following.FollowingCreatorRepository
import kr.co.vividnext.sodalive.following.FollowingCreatorViewModel
import kr.co.vividnext.sodalive.live.LiveApi
import kr.co.vividnext.sodalive.live.LiveRepository
import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendApi
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepository
import kr.co.vividnext.sodalive.live.reservation_status.LiveReservationStatusViewModel
import kr.co.vividnext.sodalive.live.room.LiveRoomViewModel
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailViewModel
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageViewModel
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.main.MainViewModel
import kr.co.vividnext.sodalive.message.MessageApi
import kr.co.vividnext.sodalive.message.MessageRepository
import kr.co.vividnext.sodalive.message.SelectMessageRecipientViewModel
import kr.co.vividnext.sodalive.message.text.TextMessageDetailViewModel
import kr.co.vividnext.sodalive.message.text.TextMessageViewModel
import kr.co.vividnext.sodalive.message.text.TextMessageWriteViewModel
import kr.co.vividnext.sodalive.message.voice.VoiceMessageViewModel
import kr.co.vividnext.sodalive.message.voice.VoiceMessageWriteViewModel
import kr.co.vividnext.sodalive.mypage.MyPageViewModel
import kr.co.vividnext.sodalive.mypage.auth.AuthApi
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.payment.CanPaymentViewModel
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusViewModel
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateViewModel
import kr.co.vividnext.sodalive.mypage.profile.nickname.NicknameUpdateViewModel
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagApi
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagRepository
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagViewModel
import kr.co.vividnext.sodalive.mypage.service_center.FaqApi
import kr.co.vividnext.sodalive.mypage.service_center.FaqRepository
import kr.co.vividnext.sodalive.mypage.service_center.ServiceCenterViewModel
import kr.co.vividnext.sodalive.network.TokenAuthenticator
import kr.co.vividnext.sodalive.report.ReportApi
import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.settings.SettingsViewModel
import kr.co.vividnext.sodalive.settings.event.EventApi
import kr.co.vividnext.sodalive.settings.event.EventRepository
import kr.co.vividnext.sodalive.settings.event.EventViewModel
import kr.co.vividnext.sodalive.settings.notice.NoticeApi
import kr.co.vividnext.sodalive.settings.notice.NoticeRepository
import kr.co.vividnext.sodalive.settings.notice.NoticeViewModel
import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsViewModel
import kr.co.vividnext.sodalive.settings.signout.SignOutViewModel
import kr.co.vividnext.sodalive.settings.terms.TermsApi
import kr.co.vividnext.sodalive.settings.terms.TermsRepository
import kr.co.vividnext.sodalive.settings.terms.TermsViewModel
import kr.co.vividnext.sodalive.user.UserApi
import kr.co.vividnext.sodalive.user.UserRepository
import kr.co.vividnext.sodalive.user.find_password.FindPasswordViewModel
import kr.co.vividnext.sodalive.user.login.LoginViewModel
import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
class AppDI(private val context: Context, isDebugMode: Boolean) {
private val baseUrl = BuildConfig.BASE_URL
private val otherModule = module {
single { GsonBuilder().create() }
single { ObjectBox(get()) }
}
private val networkModule = module {
single {
val logging = HttpLoggingInterceptor()
if (isDebugMode) {
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
} else {
logging.setLevel(HttpLoggingInterceptor.Level.NONE)
}
OkHttpClient().newBuilder()
.addInterceptor(logging)
.authenticator(TokenAuthenticator(get()))
.build()
}
single {
Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.client(get())
.build()
}
single { ApiBuilder().build(get(), CanApi::class.java) }
single { ApiBuilder().build(get(), AuthApi::class.java) }
single { ApiBuilder().build(get(), UserApi::class.java) }
single { ApiBuilder().build(get(), LiveApi::class.java) }
single { ApiBuilder().build(get(), TermsApi::class.java) }
single { ApiBuilder().build(get(), EventApi::class.java) }
single { ApiBuilder().build(get(), ReportApi::class.java) }
single { ApiBuilder().build(get(), LiveRecommendApi::class.java) }
single { ApiBuilder().build(get(), ExplorerApi::class.java) }
single { ApiBuilder().build(get(), MessageApi::class.java) }
single { ApiBuilder().build(get(), NoticeApi::class.java) }
single { ApiBuilder().build(get(), AudioContentApi::class.java) }
single { ApiBuilder().build(get(), FaqApi::class.java) }
single { ApiBuilder().build(get(), MemberTagApi::class.java) }
}
private val viewModelModule = module {
viewModel { LoginViewModel(get()) }
viewModel { SignUpViewModel(get()) }
viewModel { TermsViewModel(get()) }
viewModel { FindPasswordViewModel(get()) }
viewModel { MainViewModel(get(), get(), get(), get()) }
viewModel { LiveViewModel(get(), get(), get()) }
viewModel { MyPageViewModel(get(), get()) }
viewModel { CanStatusViewModel(get()) }
viewModel { CanChargeViewModel(get()) }
viewModel { CanPaymentViewModel(get()) }
viewModel { LiveRoomDetailViewModel(get()) }
viewModel { LiveRoomCreateViewModel(get()) }
viewModel { LiveTagViewModel(get()) }
viewModel { LiveRoomEditViewModel(get()) }
viewModel { LiveRoomViewModel(get(), get(), get()) }
viewModel { LiveRoomDonationMessageViewModel(get()) }
viewModel { ExplorerViewModel(get()) }
viewModel { UserProfileViewModel(get(), get(), get()) }
viewModel { UserFollowerListViewModel(get(), get()) }
viewModel { TextMessageViewModel(get()) }
viewModel { TextMessageWriteViewModel(get()) }
viewModel { VoiceMessageViewModel(get()) }
viewModel { VoiceMessageWriteViewModel(get()) }
viewModel { SelectMessageRecipientViewModel(get(), get()) }
viewModel { SignOutViewModel(get()) }
viewModel { NoticeViewModel(get()) }
viewModel { EventViewModel(get()) }
viewModel { NotificationSettingsViewModel(get()) }
viewModel { SettingsViewModel(get()) }
viewModel { TextMessageDetailViewModel(get()) }
viewModel { LiveReservationStatusViewModel(get()) }
viewModel { AudioContentMainViewModel(get()) }
viewModel { AudioContentViewModel(get()) }
viewModel { AudioContentOrderListViewModel(get()) }
viewModel { AudioContentUploadViewModel(get()) }
viewModel { AudioContentModifyViewModel(get()) }
viewModel { AudioContentThemeViewModel(get()) }
viewModel { AudioContentDetailViewModel(get(), get(), get(), get()) }
viewModel { AudioContentCommentListViewModel(get()) }
viewModel { AudioContentCommentReplyViewModel(get()) }
viewModel { FollowingCreatorViewModel(get()) }
viewModel { ServiceCenterViewModel(get()) }
viewModel { ProfileUpdateViewModel(get()) }
viewModel { NicknameUpdateViewModel(get()) }
viewModel { MemberTagViewModel(get()) }
viewModel { UserProfileDonationAllViewModel(get(), get()) }
viewModel { AudioContentCurationViewModel(get()) }
viewModel { AudioContentNewAllViewModel(get()) }
viewModel { AudioContentRankingAllViewModel(get()) }
}
private val repositoryModule = module {
factory { UserRepository(get()) }
factory { TermsRepository(get()) }
factory { LiveRepository(get(), get()) }
factory { EventRepository(get()) }
factory { LiveRecommendRepository(get()) }
factory { AuthRepository(get()) }
factory { CanRepository(get()) }
factory { LiveTagRepository(get()) }
factory { ReportRepository(get()) }
factory { ExplorerRepository(get()) }
factory { MessageRepository(get()) }
factory { NoticeRepository(get()) }
factory { AudioContentRepository(get(), get()) }
factory { AudioContentCommentRepository(get()) }
factory { PlaybackTrackingRepository(get()) }
factory { FollowingCreatorRepository(get(), get()) }
factory { FaqRepository(get()) }
factory { MemberTagRepository(get()) }
factory { UserProfileFantalkAllViewModel(get(), get()) }
}
private val moduleList = listOf(
networkModule,
viewModelModule,
repositoryModule,
otherModule
)
init {
startKoin {
androidContext(context)
modules(moduleList)
}
}
}

View File

@@ -0,0 +1,74 @@
package kr.co.vividnext.sodalive.dialog
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import kr.co.vividnext.sodalive.databinding.DialogLiveBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
open class LiveDialog(
activity: Activity,
layoutInflater: LayoutInflater,
title: String,
desc: String,
confirmButtonTitle: String,
confirmButtonClick: () -> Unit,
cancelButtonTitle: String = "",
cancelButtonClick: (() -> Unit)? = null,
) {
private val alertDialog: AlertDialog
val dialogView = DialogLiveBinding.inflate(layoutInflater)
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialogView.tvTitle.text = title
dialogView.tvDesc.text = desc
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) {
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,134 @@
package kr.co.vividnext.sodalive.explorer
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.Rect
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.databinding.ItemExplorerBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class ExplorerAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<ExplorerAdapter.ViewHolder>() {
private val items = mutableListOf<GetExplorerSectionResponse>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemExplorerBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetExplorerSectionResponse) {
setTitle(item)
setDesc(item)
setCreatorList(item)
}
private fun setTitle(item: GetExplorerSectionResponse) {
binding.tvTitle.text = if (
!item.coloredTitle.isNullOrBlank() &&
!item.color.isNullOrBlank()
) {
val spStr = SpannableString(item.title)
try {
spStr.setSpan(
ForegroundColorSpan(
Color.parseColor("#${item.color}")
),
item.title.indexOf(item.coloredTitle),
item.title.indexOf(item.coloredTitle) + item.coloredTitle.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
spStr
} catch (e: IllegalArgumentException) {
item.title
}
} else {
item.title
}
}
private fun setDesc(item: GetExplorerSectionResponse) {
if (item.desc != null) {
binding.llDesc.visibility = View.VISIBLE
binding.tvDesc.text = item.desc
} else {
binding.llDesc.visibility = View.GONE
}
}
private fun setCreatorList(item: GetExplorerSectionResponse) {
val adapter = ExplorerSectionAdapter(
onClickItem = onClickItem,
isVisibleRanking = item.desc != null
)
binding.rvExplorerSection.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvExplorerSection.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvExplorerSection.adapter = adapter
adapter.addItems(item.creators)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemExplorerBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetExplorerSectionResponse>) {
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,83 @@
package kr.co.vividnext.sodalive.explorer
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.GetCheersResponse
import kr.co.vividnext.sodalive.explorer.profile.GetCreatorProfileResponse
import kr.co.vividnext.sodalive.explorer.profile.PostCreatorNoticeRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.donation.GetDonationAllResponse
import kr.co.vividnext.sodalive.explorer.profile.follow.GetFollowerListResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
interface ExplorerApi {
@GET("/explorer")
fun getExplorer(
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetExplorerResponse>>
@GET("/explorer/search/channel")
fun searchChannel(
@Query("channel") channel: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetRoomDetailUser>>>
@GET("/explorer/profile/{id}")
fun getCreatorProfile(
@Path("id") id: Long,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetCreatorProfileResponse>>
@GET("/explorer/profile/{id}/donation-rank")
fun getCreatorProfileDonationRanking(
@Path("id") id: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetDonationAllResponse>>
@GET("/explorer/profile/{id}/cheers")
fun getCreatorProfileCheers(
@Path("id") creatorId: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Flowable<ApiResponse<GetCheersResponse>>
@POST("/explorer/profile/cheers")
fun writeCheers(
@Body request: PostWriteCheersRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@PUT("/explorer/profile/cheers")
fun modifyCheers(
@Body request: PutModifyCheersRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/explorer/profile/notice")
fun writeCreatorNotice(
@Body request: PostCreatorNoticeRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/explorer/profile/{id}/follower-list")
fun getFollowerList(
@Path("id") userId: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetFollowerListResponse>>
}

View File

@@ -0,0 +1,202 @@
package kr.co.vividnext.sodalive.explorer
import android.annotation.SuppressLint
import android.app.Service
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
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.FragmentExplorerBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.message.SelectMessageRecipientAdapter
import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit
class ExplorerFragment : BaseFragment<FragmentExplorerBinding>(
FragmentExplorerBinding::inflate
) {
private val viewModel: ExplorerViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: ExplorerAdapter
private lateinit var imm: InputMethodManager
private val handler = Handler(Looper.getMainLooper())
private lateinit var searchChannelAdapter: SelectMessageRecipientAdapter
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.getExplorer()
}
private fun hideKeyboard() {
handler.postDelayed({
imm.hideSoftInputFromWindow(
requireActivity().window.decorView.applicationWindowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
}, 100)
}
private fun setupView() {
adapter = ExplorerAdapter {
val intent = Intent(requireContext(), UserProfileActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, it)
startActivity(intent)
}
binding.rvExplorer.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
binding.rvExplorer.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 = 15f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 0
}
else -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
}
}
})
binding.rvExplorer.adapter = adapter
setupSearchChannelView()
}
private fun setupSearchChannelView() {
searchChannelAdapter = SelectMessageRecipientAdapter {
hideKeyboard()
val intent = Intent(requireContext(), UserProfileActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, it.id)
startActivity(intent)
}
binding.rvSearchChannel.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
binding.rvSearchChannel.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()
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
})
binding.rvSearchChannel.adapter = searchChannelAdapter
compositeDisposable.add(
binding.etSearchChannel.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
binding.ivX.visibility = if (it.length > 1) {
View.VISIBLE
} else {
View.GONE
}
if (it.length >= 2) {
viewModel.searchChannel(it.toString())
binding.rvSearchChannel.visibility = View.VISIBLE
binding.rvExplorer.visibility = View.GONE
} else {
binding.rvSearchChannel.visibility = View.GONE
binding.rvExplorer.visibility = View.VISIBLE
}
binding.tvResultX.visibility = View.GONE
}
)
binding.ivX.setOnClickListener {
hideKeyboard()
binding.etSearchChannel.setText("")
}
}
@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.responseLiveData.observe(viewLifecycleOwner) {
adapter.addItems(it.sections)
}
viewModel.searchChannelLiveData.observe(viewLifecycleOwner) {
searchChannelAdapter.items.clear()
if (it.isNotEmpty()) {
searchChannelAdapter.items.addAll(it)
binding.rvSearchChannel.visibility = View.VISIBLE
binding.tvResultX.visibility = View.GONE
} else {
binding.rvSearchChannel.visibility = View.GONE
binding.tvResultX.visibility = View.VISIBLE
}
searchChannelAdapter.notifyDataSetChanged()
}
}
}

View File

@@ -0,0 +1,96 @@
package kr.co.vividnext.sodalive.explorer
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.GetCheersResponse
import kr.co.vividnext.sodalive.explorer.profile.PostCreatorNoticeRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.donation.GetDonationAllResponse
import java.util.TimeZone
class ExplorerRepository(
private val api: ExplorerApi
) {
fun getExplorer(token: String) = api.getExplorer(authHeader = token)
fun searchChannel(channel: String, token: String) = api.searchChannel(
channel = channel,
authHeader = token
)
fun getCreatorProfile(id: Long, token: String) = api.getCreatorProfile(
id = id,
timezone = TimeZone.getDefault().id,
authHeader = token
)
fun getCreatorProfileCheers(
creatorId: Long,
page: Int,
size: Int,
token: String
): Flowable<ApiResponse<GetCheersResponse>> {
return api.getCreatorProfileCheers(
creatorId = creatorId,
page = page - 1,
size = size,
timezone = TimeZone.getDefault().id,
authHeader = token
)
}
fun writeCheers(
parentCheersId: Long?,
creatorId: Long,
content: String,
token: String
) = api.writeCheers(
request = PostWriteCheersRequest(
parentId = parentCheersId,
creatorId = creatorId,
content = content
),
authHeader = token
)
fun modifyCheers(
request: PutModifyCheersRequest,
token: String
) = api.modifyCheers(
request = request,
authHeader = token
)
fun writeCreatorNotice(notice: String, token: String) = api.writeCreatorNotice(
request = PostCreatorNoticeRequest(notice),
authHeader = token
)
fun getFollowerList(
userId: Long,
page: Int,
size: Int,
token: String
) = api.getFollowerList(
userId = userId,
page = page - 1,
size = size,
authHeader = token
)
fun getCreatorProfileDonationRanking(
id: Long,
page: Int,
size: Int,
token: String
): Single<ApiResponse<GetDonationAllResponse>> {
return api.getCreatorProfileDonationRanking(
id = id,
page = page - 1,
size = size,
authHeader = token
)
}
}

View File

@@ -0,0 +1,89 @@
package kr.co.vividnext.sodalive.explorer
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemExplorerSectionBinding
class ExplorerSectionAdapter(
private val onClickItem: (Long) -> Unit,
private val isVisibleRanking: Boolean
) : RecyclerView.Adapter<ExplorerSectionAdapter.ViewHolder>() {
private val items = mutableListOf<GetExplorerSectionCreatorResponse>()
inner class ViewHolder(
private val binding: ItemExplorerSectionBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetExplorerSectionCreatorResponse, index: Int) {
binding.root.setOnClickListener { onClickItem(item.id) }
binding.tvNickname.text = item.nickname
binding.tvTags.text = item.tags
binding.ivProfile.load(item.profileImageUrl) {
transformations(CircleCropTransformation())
placeholder(R.drawable.ic_place_holder)
crossfade(true)
}
if (isVisibleRanking) {
when (index) {
0 -> {
binding.ivBg.setImageResource(R.drawable.bg_circle_ffdc00_ffb600)
binding.ivBg.visibility = View.VISIBLE
binding.ivCrown.setImageResource(R.drawable.ic_crown_1)
binding.ivCrown.visibility = View.VISIBLE
}
1 -> {
binding.ivBg.setImageResource(R.drawable.bg_circle_ffffff_9f9f9f)
binding.ivBg.visibility = View.VISIBLE
binding.ivCrown.setImageResource(R.drawable.ic_crown_2)
binding.ivCrown.visibility = View.VISIBLE
}
2 -> {
binding.ivBg.setImageResource(R.drawable.bg_circle_e6a77a_c67e4a)
binding.ivBg.visibility = View.VISIBLE
binding.ivCrown.setImageResource(R.drawable.ic_crown_3)
binding.ivCrown.visibility = View.VISIBLE
}
else -> {
binding.ivBg.setImageResource(0)
binding.ivBg.visibility = View.GONE
binding.ivCrown.visibility = View.GONE
}
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemExplorerSectionBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], index = position)
}
override fun getItemCount() = items.size
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetExplorerSectionCreatorResponse>) {
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,91 @@
package kr.co.vividnext.sodalive.explorer
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.room.detail.GetRoomDetailUser
class ExplorerViewModel(private val repository: ExplorerRepository) : BaseViewModel() {
private val _responseLiveData = MutableLiveData<GetExplorerResponse>()
val responseLiveData: LiveData<GetExplorerResponse>
get() = _responseLiveData
private val _searchChannelLiveData = MutableLiveData<List<GetRoomDetailUser>>()
val searchChannelLiveData: LiveData<List<GetRoomDetailUser>>
get() = _searchChannelLiveData
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
fun searchChannel(channel: String) {
compositeDisposable.add(
repository.searchChannel(
channel = channel,
token = "Bearer ${SharedPreferenceManager.token}"
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_searchChannelLiveData.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 getExplorer() {
if (!_isLoading.value!!) {
_isLoading.value = true
}
compositeDisposable.add(
repository.getExplorer(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_responseLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.explorer
import com.google.gson.annotations.SerializedName
data class GetExplorerResponse(
@SerializedName("sections") val sections: List<GetExplorerSectionResponse>
)
data class GetExplorerSectionResponse(
@SerializedName("title") val title: String,
@SerializedName("coloredTitle") val coloredTitle: String?,
@SerializedName("color") val color: String?,
@SerializedName("desc") val desc: String?,
@SerializedName("creators") val creators: List<GetExplorerSectionCreatorResponse>
)
data class GetExplorerSectionCreatorResponse(
@SerializedName("id") val id: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("tags") val tags: String,
@SerializedName("profileImageUrl") val profileImageUrl: String
)

View File

@@ -0,0 +1,73 @@
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

@@ -0,0 +1,18 @@
package kr.co.vividnext.sodalive.explorer.profile
import com.google.gson.annotations.SerializedName
data class GetCheersResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("cheers") val cheers: List<GetCheersResponseItem>
)
data class GetCheersResponseItem(
@SerializedName("cheersId") val cheersId: Long,
@SerializedName("memberId") val memberId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("content") val content: String,
@SerializedName("date") val date: String,
@SerializedName("replyList") val replyList: List<GetCheersResponseItem>
)

View File

@@ -0,0 +1,95 @@
package kr.co.vividnext.sodalive.explorer.profile
import com.google.gson.annotations.SerializedName
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("cheers")
val cheers: GetCheersResponse,
@SerializedName("activitySummary")
val activitySummary: GetCreatorActivitySummary,
@SerializedName("isBlock")
val isBlock: Boolean
)
data class CreatorResponse(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("nickname") val nickname: String,
@SerializedName("tags") val tags: List<String>,
@SerializedName("introduce") val introduce: String = "",
@SerializedName("instagramUrl") val instagramUrl: String? = null,
@SerializedName("youtubeUrl") val youtubeUrl: String? = null,
@SerializedName("websiteUrl") val websiteUrl: String? = null,
@SerializedName("blogUrl") val blogUrl: String? = null,
@SerializedName("isAvailableChat") val isAvailableChat: Boolean = true,
@SerializedName("isNotification") val isNotification: Boolean,
@SerializedName("notificationRecipientCount") val notificationRecipientCount: Int
)
data class UserDonationRankingResponse(
@SerializedName("userId") val userId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImage") val profileImage: String,
@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,
@SerializedName("content") val content: String,
@SerializedName("isPaid") val isPaid: Boolean,
@SerializedName("beginDateTime") val beginDateTime: String,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("price") val price: Int,
@SerializedName("channelName") val channelName: String?,
@SerializedName("managerNickname") val managerNickname: String,
@SerializedName("isReservation") val isReservation: Boolean,
@SerializedName("isActive") val isActive: Boolean,
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean
)
data class GetAudioContentListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentListItem>
)
data class GetAudioContentListItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("title") val title: String,
@SerializedName("price") val price: Int,
@SerializedName("themeStr") val themeStr: String,
@SerializedName("duration") val duration: String?,
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentCount") val commentCount: Int,
@SerializedName("isAdult") val isAdult: Boolean
)
data class GetCreatorActivitySummary(
@SerializedName("liveCount") val liveCount: Int,
@SerializedName("liveTime") val liveTime: Int,
@SerializedName("liveContributorCount") val liveContributorCount: Int,
@SerializedName("contentCount") val contentCount: Int
)

View File

@@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.explorer.profile
import com.google.gson.annotations.SerializedName
data class MemberBlockRequest(@SerializedName("blockMemberId") val blockMemberId: Long)

View File

@@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.explorer.profile
import com.google.gson.annotations.SerializedName
data class PostCreatorNoticeRequest(
@SerializedName("notice")
val notice: String
)

View File

@@ -0,0 +1,884 @@
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
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.webkit.URLUtil
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 kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentActivity
import kr.co.vividnext.sodalive.audio_content.AudioContentAdapter
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityUserProfileBinding
import kr.co.vividnext.sodalive.explorer.profile.cheers.UserProfileCheersAdapter
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.moneyFormat
import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.reservation.complete.LiveReservationCompleteActivity
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog
import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog
import kr.co.vividnext.sodalive.report.CheersReportDialog
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
class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
ActivityUserProfileBinding::inflate
) {
private val viewModel: UserProfileViewModel by inject()
private val liveViewModel: LiveViewModel by inject()
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
private lateinit var liveAdapter: UserProfileLiveAdapter
private lateinit var audioContentAdapter: AudioContentAdapter
private lateinit var donationAdapter: UserProfileDonationAdapter
private lateinit var similarCreatorAdapter: UserProfileSimilarCreatorAdapter
private lateinit var cheersAdapter: UserProfileCheersAdapter
private lateinit var noticeWriteLauncher: ActivityResultLauncher<Intent>
private val handler = Handler(Looper.getMainLooper())
private var userId: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
super.onCreate(savedInstanceState)
imm = getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager
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()
}
bindData()
}
override fun onResume() {
super.onResume()
viewModel.getCreatorProfile(userId) { finish() }
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.tvBack.text = "채널"
binding.tvBack.setOnClickListener { finish() }
binding.ivMenu.setOnClickListener {
showOptionMenu(
this,
binding.ivMenu,
)
}
binding.layoutUserProfile.ivShare.setOnClickListener {
viewModel.shareChannel(userId = userId) {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, it)
val shareIntent = Intent.createChooser(intent, "채널 공유")
startActivity(shareIntent)
}
}
setupLiveView()
setupDonationView()
setupSimilarCreatorView()
setupFanTalkView()
setupAudioContentListView()
}
private fun hideKeyboard(onAfterExecute: () -> Unit) {
handler.postDelayed({
imm.hideSoftInputFromWindow(
window.decorView.applicationWindowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
onAfterExecute()
}, 100)
}
private fun showOptionMenu(context: Context, v: View) {
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
if (viewModel.creatorProfileLiveData.value!!.isBlock) {
inflater.inflate(R.menu.user_profile_option_menu_2, popup.menu)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_user_block -> {
viewModel.userUnBlock(userId)
}
R.id.menu_user_report -> {
showUserReportDialog()
}
R.id.menu_profile_report -> {
showProfileReportDialog()
}
}
true
}
} else {
inflater.inflate(R.menu.user_profile_option_menu, popup.menu)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_user_block -> {
showUserBlockDialog()
}
R.id.menu_user_report -> {
showUserReportDialog()
}
R.id.menu_profile_report -> {
showProfileReportDialog()
}
}
true
}
}
popup.show()
}
private fun showUserBlockDialog() {
val dialog = AlertDialog.Builder(this)
dialog.setTitle("사용자 차단")
dialog.setMessage(
"${binding.layoutUserProfile.tvNickname.text}님을 차단하시겠습니까?\n\n" +
"사용자를 차단하면 사용자는 아래 기능이 제한됩니다.\n" +
"- 내가 개설한 라이브 입장 불가\n" +
"- 나에게 메시지 보내기 불가\n" +
"- 내 채널의 팬Talk 작성불가"
)
dialog.setPositiveButton("차단") { _, _ ->
viewModel.userBlock(userId)
}
dialog.setNegativeButton("취소") { _, _ -> }
dialog.show()
}
private fun showUserReportDialog() {
val dialog = UserReportDialog(this, layoutInflater) {
viewModel.report(
type = ReportType.USER,
userId = userId,
reason = it
)
}
dialog.show(screenWidth)
}
private fun showProfileReportDialog() {
val dialog = ProfileReportDialog(this, layoutInflater) {
viewModel.report(
type = ReportType.PROFILE,
userId = userId
)
}
dialog.show(screenWidth)
}
private fun setupLiveView() {
val recyclerView = binding.layoutUserProfileLive.rvLive
liveAdapter = UserProfileLiveAdapter(
onClickParticipant = { enterLiveRoom(roomId = it.roomId) },
onClickReservation = { reservationRoom(roomId = it.roomId) }
)
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)) {
liveAdapter.itemCount - 1 -> {
outRect.bottom = 0
}
else -> {
outRect.bottom = 13.3f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = liveAdapter
}
private fun setupDonationView() {
binding.layoutUserProfileDonation.tvAll.setOnClickListener {
val intent = Intent(applicationContext, UserProfileDonationAllViewActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, userId)
startActivity(intent)
}
val recyclerView = binding.layoutUserProfileDonation.rvDonation
donationAdapter = UserProfileDonationAdapter()
recyclerView.layoutManager = LinearLayoutManager(
applicationContext,
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 = 0
outRect.right = 6.7f.dpToPx().toInt()
}
donationAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
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(
applicationContext,
UserProfileFantalkAllViewActivity::class.java
)
intent.putExtra(Constants.EXTRA_USER_ID, userId)
startActivity(intent)
}
setupCheersView()
}
private fun setupCheersView() {
binding.layoutUserProfileFanTalk.ivSend.setOnClickListener {
hideKeyboard {
viewModel.writeCheers(
creatorId = userId,
cheersContent = binding.layoutUserProfileFanTalk.etCheer.text.toString()
)
}
}
val rvCheers = binding.layoutUserProfileFanTalk.rvCheers
cheersAdapter = UserProfileCheersAdapter(
userId = userId,
enterReply = { cheersId, content ->
hideKeyboard {
viewModel.writeCheers(
parentCheersId = cheersId,
creatorId = userId,
cheersContent = content
)
}
},
modifyReply = { cheersId, content ->
hideKeyboard {
viewModel.modifyCheers(
cheersId = cheersId,
creatorId = userId,
cheersContent = content
)
}
},
modifyCheers = { cheersId, content ->
hideKeyboard {
viewModel.modifyCheers(
cheersId = cheersId,
creatorId = userId,
cheersContent = content,
)
}
},
onClickReport = { showCheersReportPopup(it) },
onClickDelete = {
SodaDialog(
activity = this@UserProfileActivity,
layoutInflater = layoutInflater,
title = "응원글 삭제",
desc = "삭제하시겠습니까?",
confirmButtonTitle = "삭제",
confirmButtonClick = {
viewModel.modifyCheers(
cheersId = it,
creatorId = userId,
isActive = false
)
},
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
}
)
rvCheers.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
rvCheers.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.bottom = 0
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 0
}
cheersAdapter.itemCount - 1 -> {
outRect.top = 10.dpToPx().toInt()
outRect.bottom = 10.dpToPx().toInt()
}
else -> {
outRect.top = 10.dpToPx().toInt()
}
}
}
})
rvCheers.adapter = cheersAdapter
}
private fun setupAudioContentListView() {
binding.layoutUserProfileAudioContent.tvAll.setOnClickListener {
val intent = Intent(applicationContext, AudioContentActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, userId)
startActivity(intent)
}
val recyclerView = binding.layoutUserProfileAudioContent.rvAudioContent
audioContentAdapter = AudioContentAdapter {
val intent = Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
startActivity(intent)
}
recyclerView.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
audioContentAdapter.itemCount - 1 -> {
outRect.bottom = 0
}
else -> {
outRect.bottom = 13.3f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = audioContentAdapter
}
private fun showCheersReportPopup(cheersId: Long) {
val dialog = CheersReportDialog(this, layoutInflater) {
if (it.isBlank()) {
Toast.makeText(
applicationContext,
"신고 이유를 선택해 주세요.",
Toast.LENGTH_LONG
).show()
} else {
viewModel.cheersReport(cheersId, reason = it)
}
}
dialog.show(screenWidth)
}
private fun bindData() {
liveViewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
liveViewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
}
}
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
}
}
}
private fun setActivitySummary(activitySummary: GetCreatorActivitySummary) {
binding.tvLiveCount.text = activitySummary.liveCount.moneyFormat()
binding.tvLiveContributorCount.text = activitySummary.liveContributorCount.moneyFormat()
binding.tvLiveTime.text = activitySummary.liveTime.moneyFormat()
binding.tvContentCount.text = activitySummary.contentCount.moneyFormat()
}
@SuppressLint("NotifyDataSetChanged")
private fun setCheers(cheers: GetCheersResponse) {
binding.layoutUserProfileFanTalk.etCheer.setText("")
cheersAdapter.items.clear()
binding.layoutUserProfileFanTalk.tvCheersCount.text = cheers.totalCount.toString()
cheersAdapter.items.addAll(cheers.cheers)
cheersAdapter.notifyDataSetChanged()
if (cheersAdapter.itemCount <= 0) {
binding.layoutUserProfileFanTalk.rvCheers.visibility = View.GONE
binding.layoutUserProfileFanTalk.tvNoCheers.visibility = View.VISIBLE
} else {
binding.layoutUserProfileFanTalk.rvCheers.visibility = View.VISIBLE
binding.layoutUserProfileFanTalk.tvNoCheers.visibility = View.GONE
}
}
@SuppressLint("SetTextI18n")
private fun setCreatorProfile(creator: CreatorResponse) {
val layoutUserProfile = binding.layoutUserProfile
if (creator.creatorId == SharedPreferenceManager.userId) {
layoutUserProfile.tvFollowerList.visibility = View.VISIBLE
layoutUserProfile.llNotification.visibility = View.GONE
layoutUserProfile.tvFollowerList.setOnClickListener {
val intent = Intent(applicationContext, UserFollowerListActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, creator.creatorId)
startActivity(intent)
}
} else {
layoutUserProfile.llNotification.visibility = View.VISIBLE
layoutUserProfile.tvFollowerList.visibility = View.GONE
}
layoutUserProfile.ivProfile.load(creator.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvBack.text = "${creator.nickname}님의 채널"
layoutUserProfile.tvNickname.text = creator.nickname
layoutUserProfile.tvTags.text = creator.tags.joinToString(" ") { "#$it" }
if (creator.websiteUrl.isNullOrBlank() || !URLUtil.isValidUrl(creator.websiteUrl)) {
layoutUserProfile.ivWebsite.visibility = View.GONE
} else {
layoutUserProfile.ivWebsite.visibility = View.VISIBLE
layoutUserProfile.ivWebsite.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(creator.websiteUrl)))
}
}
if (creator.blogUrl.isNullOrBlank() || !URLUtil.isValidUrl(creator.blogUrl)) {
layoutUserProfile.ivBlog.visibility = View.GONE
} else {
layoutUserProfile.ivBlog.visibility = View.VISIBLE
layoutUserProfile.ivBlog.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(creator.blogUrl)))
}
}
if (creator.instagramUrl.isNullOrBlank() || !URLUtil.isValidUrl(creator.instagramUrl)) {
layoutUserProfile.ivInstagram.visibility = View.GONE
} else {
layoutUserProfile.ivInstagram.visibility = View.VISIBLE
layoutUserProfile.ivInstagram.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(creator.instagramUrl)))
}
}
if (creator.youtubeUrl.isNullOrBlank() || !URLUtil.isValidUrl(creator.youtubeUrl)) {
layoutUserProfile.ivYoutube.visibility = View.GONE
} else {
layoutUserProfile.ivYoutube.visibility = View.VISIBLE
layoutUserProfile.ivYoutube.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(creator.youtubeUrl)))
}
}
if (creator.isNotification) {
layoutUserProfile.ivNotification.setImageResource(R.drawable.btn_notification_selected)
layoutUserProfile.ivNotification.setOnClickListener {
viewModel.unFollow(creator.creatorId)
}
} else {
layoutUserProfile.ivNotification.setImageResource(R.drawable.btn_notification)
layoutUserProfile.ivNotification.setOnClickListener {
viewModel.follow(creator.creatorId)
}
}
layoutUserProfile
.tvNotificationCount
.text = "팔로워 ${creator.notificationRecipientCount.moneyFormat()}"
val introduce = creator.introduce.ifBlank {
"채널 소개내용이 없습니다."
}
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 =
if (userId == SharedPreferenceManager.userId || audioContentList.isNotEmpty()) {
View.VISIBLE
} else {
View.GONE
}
if (userId == SharedPreferenceManager.userId) {
binding.layoutUserProfileAudioContent.tvTitle.text = "내 콘텐츠"
binding.layoutUserProfileAudioContent.tvNewContent.setOnClickListener {
startActivity(
Intent(
applicationContext,
AudioContentUploadActivity::class.java
)
)
}
binding.layoutUserProfileAudioContent.tvNewContent.visibility = View.VISIBLE
} else {
binding.layoutUserProfileAudioContent.tvTitle.text = "콘텐츠"
binding.layoutUserProfileAudioContent.tvNewContent.visibility = View.GONE
}
audioContentAdapter.items.clear()
audioContentAdapter.items.addAll(audioContentList)
audioContentAdapter.notifyDataSetChanged()
}
@SuppressLint("NotifyDataSetChanged")
private fun setLiveRoomList(liveRoomList: List<LiveRoomResponse>) {
if (liveRoomList.isEmpty()) {
binding.layoutUserProfileLive.root.visibility = View.GONE
} else {
binding.layoutUserProfileLive.root.visibility = View.VISIBLE
liveAdapter.items.clear()
liveAdapter.items.addAll(liveRoomList)
liveAdapter.notifyDataSetChanged()
}
}
@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()) {
binding.llUserProfileDonation.visibility = View.GONE
} else {
binding.llUserProfileDonation.visibility = View.VISIBLE
donationAdapter.items.clear()
donationAdapter.items.addAll(userDonationRanking)
donationAdapter.notifyDataSetChanged()
}
}
private fun reservationRoom(roomId: Long) {
liveViewModel.getRoomDetail(roomId) {
if (it.manager.id == SharedPreferenceManager.userId) {
showToast("내가 만든 라이브는 예약할 수 없습니다.")
} else {
if (it.isPrivateRoom) {
LiveRoomPasswordDialog(
activity = this,
layoutInflater = layoutInflater,
can = if (it.isPaid) 0 else it.price,
confirmButtonClick = { password ->
handler.postDelayed({
processLiveReservation(roomId, password)
}, 300)
}
).show(screenWidth)
} else {
if (it.price == 0 || it.isPaid) {
processLiveReservation(roomId)
} else {
LivePaymentDialog(
activity = this,
layoutInflater = layoutInflater,
title = "${it.price.moneyFormat()}캔으로 예약",
desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.",
confirmButtonTitle = "예약하기",
confirmButtonClick = { processLiveReservation(roomId) },
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
}
}
}
}
}
private fun processLiveReservation(roomId: Long, password: String? = null) {
liveViewModel.reservationRoom(roomId, password) {
val intent = Intent(
applicationContext,
LiveReservationCompleteActivity::class.java
)
intent.putExtra(Constants.EXTRA_LIVE_RESERVATION_RESPONSE, it)
startActivity(intent)
}
}
private fun enterLiveRoom(roomId: Long) {
val onEnterRoomSuccess = {
runOnUiThread {
val intent = Intent(applicationContext, LiveRoomActivity::class.java)
intent.putExtra(Constants.EXTRA_ROOM_ID, roomId)
startActivity(intent)
}
}
liveViewModel.getRoomDetail(roomId) {
if (it.channelName != null) {
if (it.manager.id == SharedPreferenceManager.userId) {
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
} else if (it.price == 0 || it.isPaid) {
if (it.isPrivateRoom) {
LiveRoomPasswordDialog(
activity = this,
layoutInflater = layoutInflater,
can = 0,
confirmButtonClick = { password ->
liveViewModel.enterRoom(
roomId = roomId,
onSuccess = onEnterRoomSuccess,
password = password
)
}
).show(screenWidth)
} else {
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
}
} else {
if (it.isPrivateRoom) {
LiveRoomPasswordDialog(
activity = this,
layoutInflater = layoutInflater,
can = it.price,
confirmButtonClick = { password ->
liveViewModel.enterRoom(
roomId = roomId,
onSuccess = onEnterRoomSuccess,
password = password
)
}
).show(screenWidth)
} else {
LivePaymentDialog(
activity = this,
layoutInflater = layoutInflater,
title = "${it.price.moneyFormat()}캔으로 입장",
desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.",
confirmButtonTitle = "결제 후 입장",
confirmButtonClick = {
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
},
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
}
}
}
}
}
}

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