Compare commits

667 Commits

Author SHA1 Message Date
1720173a16 fix: fcm 데이터 수신 수정
- data-only, notification+data 방식 모두 동일하게 딥링크가 적용되도록 수정
2025-05-09 11:56:17 +09:00
60190e099a feat: fcm 데이터 수신 방식 수정
- data-only 메시지만 수신 방식에서 notification + data로 수신 방식 변경
2025-05-08 19:44:32 +09:00
db4bd56df2 versionCode 165, versionName "1.36.0" 2025-04-29 16:51:55 +09:00
affb6865a8 fix: 라이브 - only 메뉴판 on/off 시 변경 되지 않던 버그 수정 2025-04-29 14:18:24 +09:00
cab10717e9 fix: 콘텐츠 PlayCount 기록 기준을 12초에서 4초로 수정 2025-04-29 13:56:59 +09:00
ee870c4366 versionCode 164, versionName "1.35.0" 2025-04-25 15:56:34 +09:00
d61854f972 fix: 콘텐츠 상세 - 우측 상단에 포인트 아이콘 크기 수정 2025-04-24 20:32:49 +09:00
e45df2bac5 versionCode 163, versionName 1.35.0 2025-04-24 13:36:45 +09:00
009e2080fc feat: 콘텐츠 상세 - 포인트가 사용 가능한 경우 커버이미지 우측 상단에 포인트 아이콘으로 표시 2025-04-24 11:18:39 +09:00
f265732741 feat: 마이페이지 - 포인트 내역 추가 2025-04-23 21:57:28 +09:00
d11326233f feat: 포인트 지급 팝업 - 확인 버튼 터치시 팝업만 닫히도록 수정 2025-04-22 22:55:49 +09:00
5536236100 feat: 회원가입 or 소셜 로그인 시 pushToken 추가 2025-04-22 18:11:44 +09:00
b077a361b9 feat: 포그라운드 상태에서 FCM data-only 수신 시 인앱 팝업 표시 2025-04-22 18:06:47 +09:00
c0ad98f285 콘텐츠 상세
- 새로고침시 구매자의 데이터가 계속 추가되는 버그 수정
2025-04-16 18:53:16 +09:00
11307eae3b versionCode 162, versionName "1.34.2" 2025-04-16 18:41:35 +09:00
14da5f6a19 라이브 정보 수정
- 배경만 단독으로 변경되지 않는 버그 수정
2025-04-16 18:35:50 +09:00
065f7ee038 회원탈퇴
- 소셜로그인 탈퇴 안내 문구 추가
2025-04-15 19:42:44 +09:00
710015d89e 라이브 입장 메시지 on/off 스위치 추가
- 라이브 정보 수정 가장 아래에 입장 메시지 on/off 스위치 추가
2025-04-15 17:15:23 +09:00
c5a173138c 한정판 콘텐츠 상세
- 해당 콘텐츠를 업로드 한 크리에이터가 콘텐츠 구매자를 볼 수 있는 UI 추가
2025-04-14 15:03:58 +09:00
dfb2c903a4 콘텐츠 상세
- 10초 전/후로 이동 기능 추가
2025-04-08 13:19:52 +09:00
01dc0cabbe 회원가입
- 소셜 로그인과 같은 크기의 버튼 추가
2025-04-07 20:46:02 +09:00
d4796257b3 카카오 로그인 추가 2025-04-07 19:20:57 +09:00
a9885874ee 구글 로그인 추가 2025-04-04 18:40:22 +09:00
44e3f0c171 versionCode 157, versionName 1.33.0 2025-04-02 11:04:41 +09:00
62b15609ff 콘텐츠 상세
- 이전화/다음화 버튼 추가
2025-04-01 20:58:21 +09:00
bddf7b750b 구간반복 기능 추가 2025-04-01 14:25:45 +09:00
c7af522cfb 검색 UI 추가 2025-03-31 12:47:52 +09:00
e4b0dbae82 콘텐츠 메인 탭 홈
- 레거시 검색 UI 제거
2025-03-27 01:26:57 +09:00
c0c5d1afec 콘텐츠 메인 탭 홈
- 왼쪽 상단 로고 제거
- 오른쪽 상단 충전 페이지 이동 아이콘 추가
- 오른쪽 상단 모닝콜, 보관함 아이콘 제거
2025-03-27 01:12:51 +09:00
4d87544b7b pid를 심어놓은 광고를 타고 들어온 경우 항상 AppLaunch 이벤트를 실행하는 코드 추가 2025-03-26 17:19:35 +09:00
b50df2cdf3 라이브
- 크리에이터의 경우 라이브 만들기 버튼이 바로 보이도록 수정
2025-03-24 21:25:31 +09:00
c3d5c12e6b 라이브
- 라이브 중인 아이템 터치시 비로그인 상태에서는 로그인 페이지로 이동하는 기능 추가
2025-03-24 21:17:46 +09:00
0077f172b6 콘텐츠 메인 홈
- 아이콘과 글자크기 수정
2025-03-24 21:12:35 +09:00
089534fb47 - 라이브 우측 상단의 메시지 아이콘 색상 변경 2025-03-24 21:07:04 +09:00
bd851f6afd 콘텐츠 메인 홈
- 비 로그인 상태에서 인기순위 날짜 보이지 않도록 수정
2025-03-24 20:53:10 +09:00
2d69f27a25 로그인
- 로그인 액션시 키보드가 바로 숨겨지도록 수정
2025-03-22 06:53:10 +09:00
489b968ea3 라이브 메인
- 로그인 하지 않고도 페이지를 조회할 수 있도록 수정
2025-03-22 06:47:56 +09:00
7ab2779805 비밀번호 재설정
- 입력창 UI 수정
2025-03-22 05:26:41 +09:00
964b92f83a 오디션 메인
- 로그인 하지 않고도 페이지를 조회할 수 있도록 수정
2025-03-22 05:21:36 +09:00
4445a745bf 고객센터
- 카테고리 배경색, dropdown 아이콘 변경
2025-03-22 05:03:12 +09:00
3b3327be7b 마이 페이지
- 로그인 했을 때만 데이터를 조회하도록 수정
2025-03-22 04:55:53 +09:00
70fe5a4441 콘텐츠 메인
- 로그인 하지 않고도 페이지를 조회할 수 있도록 수정
2025-03-22 04:11:01 +09:00
22f90b2e40 회원가입, 로그인 입력창 속성 수정 2025-03-22 00:29:23 +09:00
dfe3b291a1 회원가입 단계 간소화 2025-03-21 04:42:08 +09:00
9331ba1276 로그인 UI
- 입력창 크기 및 UI 수정
2025-03-21 02:11:45 +09:00
7c39d6c53a 온보딩 페이지 제거 2025-03-20 18:38:53 +09:00
e941f7c940 라이브 탭
- 상단에 메시지 버튼 추가
2025-03-20 16:26:15 +09:00
e0e935cf29 메인
- 메시지 탭 제거
2025-03-20 15:18:12 +09:00
74c2db6ceb 콘텐츠 메인 홈
- 콘텐츠 마켓 -> 보이스온 으로 변경
2025-03-20 13:36:41 +09:00
506f446b60 채널 상세
- 19금 콘텐츠 보기 여부 적용
2025-03-19 23:23:03 +09:00
68a777c8df 시리즈 상세, 시리즈 리스트, 시리즈 전체회차 듣기
- 19금 콘텐츠 보기 여부 적용
2025-03-19 22:35:45 +09:00
10c215d9bd 콘텐츠 전체보기
- 19금 콘텐츠 보기 여부 적용
2025-03-19 22:28:10 +09:00
28b7aaae9f 콘텐츠/팬 Talk 댓글 수정 오류
- 댓글 수정 모드 상태에서 다른 댓글 쓰기 혹은 삭제 등의 액션을 해서 댓글 수의 변화가 있을 때 내 글이 아닌 다른 사람의 글이 수정 모드로 보이는 버그 수정
2025-03-18 23:10:20 +09:00
e1950eba2b 콘텐츠
- 남성향, 여성향 콘텐츠 설정 적용
- 19금 콘텐츠 보기 여부 설정 적용
2025-03-18 16:43:04 +09:00
b08eb896a7 노티플라이
- 앱푸시를 보내면 빈 알림이 표시되는 버그 수정
2025-03-12 18:09:54 +09:00
8c013f7126 파이어베이스 트래킹
- login 트래킹 위치 수정
- marketing_pid 값 추가
2025-03-12 16:26:22 +09:00
01d96a19b9 Notifly 설정 추가
- logout
- 메인 화면에 진입할 때마다 데이터 업데이트
2025-03-12 16:05:41 +09:00
10208fada8 Notifly 기본 설정 추가 2025-03-12 03:08:12 +09:00
d430f5d543 UTM 기록 이벤트 변경
- Firebase 이벤트 상수인 CAMPAIGN_DETAILS를 사용했을 때 데이터가 기록되지 않아 커스텀 이벤트인 ad_partner_campaign_detail로 UTM 파라미터 기록
2025-03-12 00:58:29 +09:00
536c76b9bf UTM Campaign 파라미터를 Analytics에 설정된 파라미터 상수값 사용 2025-03-11 23:44:07 +09:00
c0c31a23cc Firebase 트래킹 추가
- 회원가입, 로그인
2025-03-10 16:09:55 +09:00
6065b353fd 라이브, 콘텐츠, 채널 공유하기
- sns 공유시 보여줄 og data 제거
- shorturl이 적용되지 않은 상태에서 url이 너무 길기 떄문데 임시 적용
2025-03-07 14:42:36 +09:00
7885200af4 라이브, 콘텐츠, 채널 공유하기
- 파라미터 키, 값 각각 인코딩 적용
2025-03-07 02:50:36 +09:00
415383393a 앱스플라이어 딥링크
- 가장 먼저 실행되는 Application 영역으로 이동
2025-03-07 02:24:33 +09:00
f790264e44 versionCode 148
versionName "1.30.0"
2025-03-07 00:45:02 +09:00
d8afdecc89 라이브, 콘텐츠, 채널 공유 재추가
- AppsFlyer OneLink로 공유링크 생성
2025-03-07 00:41:29 +09:00
46e1efff2a Firebase Analytics
- UTM 기록
2025-03-06 00:55:03 +09:00
b39857cf24 딥링크만 처리하는 액티비티 추가 2025-03-05 04:13:01 +09:00
c5e60785da 앱스플라이어 딥링크
- series, live, content, channel 딥링크의 경우 해당 페이지로 이동하는 기능 추가
2025-03-05 01:14:24 +09:00
e7cc1df201 앱스플라이어 딥링크
- marketingPid는 값이 있을 때만 SharedPreference에 저장하도록 수정
2025-03-05 00:52:54 +09:00
d9fad70201 앱 메인 - pid 업데이트 API
- 메인 페이지 접속 후 3초 이후에 업데이트 되도록 수정
2025-03-04 14:27:51 +09:00
95c77d531d 앱 메인
- pid 업데이트 API 적용
2025-03-04 13:29:03 +09:00
658f304ce5 회원가입
- marketing pid 추가
2025-03-04 12:16:49 +09:00
82f71f9a07 앱스플라이어
- applink, urischeme 설정
2025-03-02 18:01:22 +09:00
1de374de3e 앱스플라이어
- sdk 추가
- 딥링크 & 디퍼드 딥링크에서 값 받아와서 SharedPreference에 저장
- test applink, test urischeme 설정
2025-03-02 17:45:55 +09:00
9cef92199d 파이어베이스 다이나믹 링크 제거 2025-03-02 15:52:57 +09:00
fb60574f3d 콘텐츠 플레이어 - 재생목록
- 아이템을 터치했을 때 현재 index도 같이 변경되도록 수정해서 이전/이후 재생시 현재 아이템 기준으로 이전/이후 콘텐츠가 재생되도록 수정
2025-02-26 17:19:47 +09:00
2d92c6a849 콘텐츠 플레이어
- 재생목록 보일 때 제목과 크리에이터 영역까지 재생목록이 차지하도록 수정
2025-02-26 16:57:54 +09:00
cbda2b196a 콘텐츠 플레이어
- 10초 이전/이후로 이동하는 기능 추가
2025-02-25 21:50:41 +09:00
9d042ff75f 콘텐츠 플레이어 - 재생 목록
- 현재 재생 중이지 않은 콘텐츠 터치시 터치한 콘텐츠를 재생하는 기능 추가
2025-02-25 21:25:06 +09:00
5d7db2d7e9 콘텐츠 플레이어
- 현재 재생 중인 콘텐츠 배경 변경
2025-02-25 18:30:20 +09:00
7d15179be0 콘텐츠 메인 홈
- 테마 탭 순서 변경
2025-02-25 13:40:47 +09:00
490bcd87af 메타(페이스북) SDK 추가 2025-02-25 13:34:08 +09:00
4623d0abd2 콘텐츠 메인 홈
- 크리에이터가 아닌데 콘텐츠 업로드 버튼이 보이는 버그 수정
2025-02-24 16:22:08 +09:00
1b47e38f79 콘텐츠 메인 새로운 단편 전체보기 제목 변경
- 무료 콘텐츠 조회시 -> 새로운 무료 콘텐츠
- 기본 -> 새로운 단편
2025-02-22 12:34:31 +09:00
09ca0487f8 콘텐츠 메인
- 콘텐츠 미니 플레이어 추가
2025-02-22 12:12:01 +09:00
3a7df3f16e 콘텐츠 메인 새로운 단편 전체보기 제목 변경
- 무료 콘텐츠 조회시 -> 새로운 무료 콘텐츠
- 기본 -> 새로운 단편
2025-02-22 11:44:32 +09:00
63646b0d19 콘텐츠 메인 라이브 다시듣기 - 새로운 라이브 다시듣기 전체보기
- 다시보기 -> 다시듣기
2025-02-22 11:41:27 +09:00
9b511b9d18 스플래시 문구 변경 2025-02-22 11:40:23 +09:00
0180a384e1 콘텐츠 대여 기간 안내 변경
- 15일 -> 5일
2025-02-22 11:09:37 +09:00
c79eb90500 콘텐츠 메인 시리즈 탭 - 완결 시리즈
- URL 변경
2025-02-22 04:28:35 +09:00
6129982df4 콘텐츠 메인 단편 탭 - 새로운 콘텐츠 전체보기
- 새로운 콘텐츠가 없으면 '새로운 콘텐츠가 없습니다.' 문구 출력
2025-02-19 18:29:51 +09:00
8ed9e08a60 콘텐츠 메인 시리즈 탭 - 일간 랭킹
- 아이템 사이즈, 아이템 사이 간격 수정
2025-02-19 03:53:09 +09:00
c14150191f 콘텐츠 메인 시리즈 탭 - 완결 시리즈 전체보기
- 한줄에 아이템 3개씩 보이도록 수정
- 크리에이터 표시
2025-02-19 02:34:43 +09:00
b7598627b1 콘텐츠 메인 시리즈 탭
- 일간 랭킹 아이템 이미지 사이즈 고정
2025-02-19 02:00:07 +09:00
475ddc21b3 콘텐츠 메인 홈 탭
- 카테고리 아이콘, 글씨 사이즈 업
2025-02-19 01:55:59 +09:00
61cb208f2f 콘텐츠 메인 다시듣기 탭
- 순위 제거
2025-02-19 01:55:35 +09:00
d0e01a83e3 콘텐츠 메인 ASMR 탭
- 순위 제거
2025-02-19 01:40:53 +09:00
7bb8f9c5af 콘텐츠 메인 단편 탭
- 태그별 추천 콘텐츠 영역 추가
2025-02-18 23:44:39 +09:00
304e6e166a 콘텐츠 메인 무료 탭
- 채널별 추천 무료 콘텐츠 UI 추가
2025-02-18 02:08:06 +09:00
e75602fc8d 콘텐츠 메인 단편 탭
- 큐레이션 추가
2025-02-17 23:40:37 +09:00
e46c34558e 콘텐츠 메인 탭 - 새로운 콘텐츠 전체보기
- 전체 개수 추가
2025-02-17 21:40:16 +09:00
0e70ed2661 콘텐츠 메인 탭 - 채널별 **
- 콘텐츠 가격과 러닝 타임 표시
2025-02-17 18:42:17 +09:00
04569f1e7e 콘텐츠 메인 탭 - 채널별 **
- RecyclerView와 GridLayout으로 변경
2025-02-17 18:05:59 +09:00
189e757100 콘텐츠 메인 시리즈 탭
- 완결 시리즈가 일간 순위에 잘못 표시되던 버그 수정
2025-02-17 09:14:25 +09:00
623ff545b5 콘텐츠 메인 탭 - 채널별 ***
- 채널 표시 간격 22dp로 수정
2025-02-17 09:04:56 +09:00
bbd972d860 콘텐츠 메인
- 콘텐츠 배너 상단 마진 제거
2025-02-17 08:40:04 +09:00
bbf0b26025 사용하지 않는 로그 제거 2025-02-15 03:05:14 +09:00
e5acc5468f 콘텐츠 메인 다시듣기 탭
- 채널별 라이브 다시듣기 추가
2025-02-15 03:04:29 +09:00
28ec227658 콘텐츠 메인 ASMR 탭
- 채널별 추천 ASMR 추가
2025-02-15 02:53:40 +09:00
022b51488f 새로운 ASMR, 라이브 다시듣기 전체보기 페이지 추가 2025-02-15 01:13:22 +09:00
2573a50190 새로운 알람 전체보기 페이지 추가 2025-02-15 00:43:01 +09:00
d612bbb0f2 완결 시리즈
- 페이지 추가
2025-02-14 19:09:37 +09:00
ef32eb70dd 새로운 콘텐츠 전체보기
- 무료 플래그를 추가하여 무료콘텐츠만 조회가 가능하도록 수정
2025-02-14 18:31:36 +09:00
b331048dec 콘텐츠 메인 - 무료 탭
- 크리에이터 소개 전체보기 페이지 추가
2025-02-14 15:31:01 +09:00
f5979ef745 콘텐츠 메인 - 시리즈 탭 - 채널별 추천 시리즈
- 채널 터치 액션 추가
2025-02-14 14:21:24 +09:00
2ff2c2224a 콘텐츠 메인 - 시리즈 탭
- 장르별 추천 시리즈 장르 터치 액션 추가
2025-02-14 13:58:12 +09:00
e1028ada43 콘텐츠 메인 - 시리즈 탭
- 오리지널 오디오 드라마 전체보기 페이지 추가
2025-02-14 05:22:08 +09:00
cc10bce487 콘텐츠 메인 - 시리즈 탭
- 처음 로딩시 섹션 숨김 처리
2025-02-14 04:44:25 +09:00
46ae544cfd 콘텐츠 메인
- 무료 탭 UI 구성
2025-02-14 01:56:28 +09:00
b2bf9a4a4a 콘텐츠 메인
- 다시듣기 탭 UI 구성
2025-02-13 22:53:10 +09:00
0ed812c6f8 콘텐츠 메인
- ASMR 탭 UI 구성
2025-02-13 22:47:47 +09:00
a375839506 콘텐츠 메인
- 선택된 탭으로 탭이 스크롤 되도록 수정
2025-02-13 19:12:14 +09:00
519d0cd02a 콘텐츠 메인
- 선택된 탭으로 탭이 스크롤 되도록 수정
2025-02-13 19:08:18 +09:00
363d611e0f 콘텐츠 메인
- 단편 탭 UI 구성
2025-02-13 16:30:40 +09:00
f75134c7e7 콘텐츠 메인
- 시리즈 탭 UI 구성
2025-02-13 13:27:45 +09:00
42e4c4649b 콘텐츠 메인
- 탭 구성
2025-02-12 01:55:10 +09:00
5469d288ba 콘텐츠 메인 업데이트
- 홈 UI 업데이트
2025-02-10 03:08:10 +09:00
c7b238f975 본인인증 완료
- 남성이면 남성향, 여성이면 여성향으로 콘텐츠 보기 설정이 변경되도록 수정
2025-02-06 20:28:50 +09:00
09209c150f 본인인증 완료
- 앱 재시작 되도록 수정
2025-02-03 18:54:23 +09:00
2802448fe9 오디션 이용방법 링크 추가 2025-01-20 22:16:21 +09:00
a167e976a8 콘텐츠 배너
- 시리즈에 연결되는 배너 타입 추가
2025-01-17 14:35:54 +09:00
8c848bd1ef ScrollView 내부의 최상위 레이아웃을 ConstraintLayout에서 LinearLayout으로 변경 2025-01-15 23:45:29 +09:00
d860cd0552 versionName 1.26.0, versionCode 138 2025-01-09 05:32:32 +09:00
7d2bed2c8a versionName 1.26.0, versionCode 137 2025-01-09 04:53:27 +09:00
35f90884d2 콘텐츠 메인, 채널 검색
- 검색창 힌트 글자색 eeeeee -> 555555 변경
2025-01-09 04:32:23 +09:00
2667e29bb9 콘텐츠 메인
- 채널 검색 추가
2025-01-09 03:54:02 +09:00
d00a5475ce 콘텐츠 메인
- 사용하지 않는 UI 제거
2025-01-09 03:01:29 +09:00
809c924bfc 푸시를 눌러서 온 오디션
- 콘텐츠 탭으로 이동하지 못하던 버그 수정
2025-01-09 01:34:49 +09:00
bb944f7903 오디션 - 오디션 알림
- 푸시 알림을 누르면 오디션 탭으로 이동
2025-01-08 23:10:20 +09:00
273ddb8b97 오디션
- 오디션 알림 받기 설정 추가
2025-01-08 22:11:49 +09:00
4180736065 오디션 상세
- 오디션 캐릭터 리스트 영역이 오디션 캐릭터 제목 밑으로 오도록 수정
2025-01-08 03:56:40 +09:00
7b129309e5 커뮤니티 댓글
- 대댓글의 닉네임과 댓글의 글자색상, 글자크기를 원댓글과 동일하게 수정
2025-01-08 03:44:19 +09:00
ece1d89780 콘텐츠 댓글
- 대댓글의 닉네임과 댓글의 글자색상, 글자크기를 원댓글과 동일하게 수정
2025-01-08 03:38:21 +09:00
26930c23df 오디션 정보, 오디션 캐릭터 정보
- 줄간격 5로 설정
2025-01-08 03:32:53 +09:00
1b64b84bdc 오디션 지원 버튼 텍스트
- 오디션 지원하기 -> 오디션 지원
2025-01-08 02:58:31 +09:00
b3351ceb8b 마이페이지
- 내 보관함 이동 버튼 추가
2025-01-08 02:14:01 +09:00
ae378409b9 시리즈 상세
- 크리에이터 프로필 터치시 채널로 이동
2025-01-08 01:21:08 +09:00
43f2c8b1e0 알람, 콘텐츠 보관함
- 아이콘 크기 72 -> 84로 변경
2025-01-08 01:10:22 +09:00
20b627202e 오디션, 오디션 상세
- 오디션 리스트, 오디션 캐릭터 리스트 아이템 사이 간격 16.7 -> 25로 변경
2025-01-08 00:53:54 +09:00
9cafb13b50 콘텐츠 메인
- 인기 크리에이터 추가
2025-01-05 17:21:11 +09:00
2bec9d4595 오디션 리스트
- ON과 OFF 사이에 구분 줄 추가 및 간격 수정
2025-01-05 13:21:12 +09:00
bf6884c60c 오디션 지원 다이얼로그
- 동의하기 글자와 체크 이미지 정렬
2025-01-04 02:18:45 +09:00
8441e4e5dd 오디션 이미지
- Coil을 사용하지 않고 Glide로 변경
2025-01-04 02:01:58 +09:00
a4d1d69a97 오디션 투표
- 안내 팝업의 문구 가운데 정렬
2025-01-04 01:37:57 +09:00
b72d692221 오디션 배역 리스트
- 모집 완료 된 배역을 터치 되지 않도록 수정
2025-01-04 01:30:10 +09:00
0f9a03fef9 오디션 지원 리스트
- 닉네임과 재생 ProgressBar의 간격 2.3 -> 8dp 로 수정
2025-01-04 01:27:27 +09:00
87f02918f8 채널 상세
- 크리에이터가 아닌 유저의 채널에 접근했을 때는 팬톡과 유저 정보만 보이도록 수정
2025-01-04 01:22:16 +09:00
b5546b4957 오디션 배역 상세
- 오디션 지원하기 버튼이 오디션 지원현황을 가리지 않도록 최하단 마진을 0에서 33dp로 수정
2025-01-03 21:47:56 +09:00
83e08b3437 오디션 지원자
- recyclerview.setHasFixedSize(false)로 지정하여 데이터가 0개에서 1개가 될 때 갱신되지 않는 버그 수정
2025-01-03 20:00:24 +09:00
931a9433f3 오디션 지원자
- 오디오 재생시 시간과 ProgressBar 추가
2025-01-03 19:47:10 +09:00
679e9ed349 오디션 지원자 투표
- 재생/일시정지, 투표 터치 영역 변경
2025-01-03 17:45:22 +09:00
5e6225d14c 오디션 지원자 투표
- 안내 팝업 Dialog 추가
2025-01-03 17:42:19 +09:00
b0a97ab941 오디션 지원자 콘텐츠 재생/정지 기능 추가 2025-01-03 11:30:42 +09:00
92b201a6fa 커뮤니티 콘텐츠 재생
- 플레이어(재생목록)의 재생이 멈추도록 코드 수정
2025-01-03 10:11:41 +09:00
ee396b3102 오디션 투표
- 안내 팝업 추가
- 투표시 데이터가 변경되지 않던 버그 수정
2025-01-03 08:50:28 +09:00
4e14765e94 오디션 지원 기능 추가 2025-01-03 07:48:34 +09:00
c6ef5970a5 오디션 배역 상세
- 지원 리스트 UI 작성
- 투표 API 적용
2025-01-03 04:54:46 +09:00
968428cfe0 배역 상세 페이지 추가 2025-01-02 17:02:15 +09:00
5d6ea6774b 오디션 상세 페이지 추가 2024-12-31 12:49:16 +09:00
4331792b75 오디션 탭 추가 2024-12-31 07:31:37 +09:00
2506ba4353 서비스가 종료되었더라도 재생버튼을 누르면 다시 실행되도록 수정 2024-12-18 20:29:26 +09:00
187f1f9d37 최근 사용한 앱에서 태스크 종료
- service가 종료되도록 코드 추가
2024-12-18 18:24:02 +09:00
6dd9520bda 재생 목록 만들기 / 수정
- 설명 입력창 minHeight 제거
2024-12-18 17:40:14 +09:00
5f0323e0a9 미디어 알림 삭제 후 재생 버튼 터치시 재생이 되지 않던 버그 수정 2024-12-18 17:01:41 +09:00
f78a13bd2c MediaSession 알림 터치시 메인 페이지로 이동 2024-12-18 16:47:57 +09:00
46ec9ff999 재생 목록 수정
- 콘텐츠 수정시 이전 콘텐츠가 재생목록에서 제거되지 않던 버그 수정
2024-12-18 15:46:23 +09:00
a7f67dc72e 플레이어 재생 완료
- 다음 재생할 콘텐츠가 없을 때 서비스를 종료해 플레이어에서 재생 및 이전/다음 콘텐츠 재생이 안되는 버그 수정
2024-12-18 15:23:08 +09:00
8867fe9a1c 구매목록
- orderType 파라미터 제거
2024-12-18 04:27:31 +09:00
6da8846460 재생 목록 수정
- 콘텐츠가 삭제 되지 않는 버그 수정
2024-12-18 01:26:41 +09:00
057e21570b 재생 목록 플레이어
- 재생 목록 리스트 추가
2024-12-13 23:21:58 +09:00
8c8b8c1747 개별 콘텐츠 재생 <-> 재생목록 재생
- 기존에 재생 중인 콘텐츠(재생목록)를 멈추고 신규로 재생한 콘텐츠(재생목록)가 단독으로 재생되도록 수정
2024-12-13 22:58:53 +09:00
cb1dadab9d 플레이어
- 커버이미지 roundCorder 12dp
2024-12-13 22:47:26 +09:00
29595670af 메인페이지
- 하단에 미니 플레이어 추가
2024-12-13 22:30:05 +09:00
6da3192fe8 로그아웃, 라이브 입장
- 플레이어 서비스 중단 로직 추가
2024-12-13 20:58:57 +09:00
c83a865032 재생목록 상세
- 하단에 미니 플레이어 추가
2024-12-13 20:53:49 +09:00
316c4399ce 재생목록 플레이어
- 재생, 이전, 다음 기능 추가
2024-12-13 13:28:27 +09:00
40e82a3796 불필요한 클래스 제거 2024-12-12 12:37:06 +09:00
a7a7eb3e3f 콘텐츠 플레이어
- BottomSheet 로 수정
- 콘텐츠 url 생성 api
2024-12-07 00:17:25 +09:00
a4b1ef0005 재생 목록 수정 페이지 추가 2024-12-05 18:52:28 +09:00
326ad01983 재생 목록 만들기 - 콘텐츠 리스트
- 소장 콘텐츠만 나오도록 수정
2024-12-04 19:14:23 +09:00
6fbe7da71e 재생 목록 상세
- 삭제 기능 추가
2024-12-04 18:10:50 +09:00
4012b44344 재생 목록 상세 콘텐츠
- 크리에이터 닉네임 추가
2024-12-04 13:52:21 +09:00
2ee62ac900 내 보관함
- 탭 왼쪽 정렬
2024-12-04 11:50:07 +09:00
d9e39f88a8 재생목록 상세
- 콘텐츠 리스트 표시
2024-12-04 11:45:27 +09:00
848f0b44f6 새 재생목록 만들기
- 콘텐츠 추가 로직
- 재생목록 생성 API 연동
2024-12-04 11:14:43 +09:00
40335fb7ff 새 재생목록 만들기
- 새로운 콘텐츠 추가 페이지 추가
2024-12-03 23:49:25 +09:00
ad5a84c3b8 새 재생목록 만들기
- 페이지 추가
2024-12-03 14:59:17 +09:00
f7073ec422 폰트 파일 변경
- 공식 페이지에서 다시 가져옴
2024-12-03 14:58:56 +09:00
943660f98e 콘텐츠 보관함(구매목록)
- 중복구현 제거
2024-11-29 15:41:57 +09:00
5640a28fdb 내 보관함
- 구매목록 UI 연결
2024-11-29 15:34:18 +09:00
ab89b6e21a 재생목록과 구매목록이 탭이 있는 내 보관함 페이지 추가 2024-11-29 15:10:10 +09:00
b38ada0b73 재생목록 상세 페이지 2024-11-29 12:15:32 +09:00
f72f894727 재생목록 리스트 페이지 2024-11-28 17:58:10 +09:00
cf4854c78d 콘텐츠 플레이어
- 파일명, 클래스명 수정
2024-11-26 12:44:04 +09:00
3004018ea9 콘텐츠 플레이 리스트
- 플레이어 UI 추가
2024-11-26 02:30:03 +09:00
6b6280a782 팬토크
- 내 글 수정 모드 상태에서 해당 글을 삭제하면 수정모드가 풀리지 않아 다른 사람 글을 수정할 수 있는 것처럼 보이는 버그 수정
2024-11-26 01:02:49 +09:00
f9577909ff 콘텐츠 댓글
- 내 댓글 수정 모드 상태에서 해당 댓글을 삭제하면 수정모드가 풀리지 않아 다른 사람 댓글을 수정할 수 있는 것처럼 보이는 버그 수정
2024-11-26 00:18:27 +09:00
49f9310fc3 라이브 비밀 미션
- 채팅 이모지 변경
2024-11-21 02:30:53 +09:00
abc12e38b5 프로필 수정
- 비밀번호 변경, 닉네임 변경, 관심사 선택 글자색, 배경색을 버튼색(#3bb9f1)로 수정
2024-11-21 01:24:24 +09:00
57c66955f6 firebase fetch 간격 수정
- 300초 -> 60초
2024-11-21 01:07:12 +09:00
5112117155 점검중 플래그 추가 2024-11-21 00:58:52 +09:00
8d67571319 라이브 상세
- website, blog, instagram, youtube icon 색상 button 색으로 변경
2024-11-21 00:43:24 +09:00
1885482055 비밀 미션
- 비밀 미션 채팅 배경색 : 보라색으로 고정
2024-11-21 00:31:56 +09:00
b67df96c85 비밀 미션
- 비밀 미션 이용 최소 금액 : 10캔 으로 설정
2024-11-21 00:26:08 +09:00
71f5dc9ef1 versionCode 125, versionName: 1.24.0 2024-11-20 23:43:35 +09:00
6efb5a679d 의존성 라이브러리 version up 2024-11-15 21:57:13 +09:00
9ae34fa667 라이브 방
- 비밀 미션 채팅 배경색 변경
2024-11-12 02:35:31 +09:00
3136b47838 하트 랭킹 추가 2024-11-12 00:32:21 +09:00
c15e9c203e 후원 히스토리
- 내 후원과 다른 사람의 후원 배경색 분리 (59548f)
2024-11-11 14:11:25 +09:00
c72c1e16fd 비밀후원 -> 비밀미션 2024-11-08 23:20:29 +09:00
03712558e8 콘텐츠 구매확인 다이얼로그
- 대여만 가능시 100% 가격을 표시하도록 수정
2024-11-08 21:03:25 +09:00
6689932393 콘텐츠 상세
- 소장만, 대여만 가능시 구매하기 버튼 배경색 변경
- 소장만, 대여만 가능시 구매하기 버튼을 터치하면 바로 구매확인 다이얼로그 표시
2024-11-08 16:47:15 +09:00
140f933db7 콘텐츠 업로드
- 소장만 가능한 콘텐츠 업로드 기능 추가
2024-11-08 16:11:35 +09:00
6f1dcb4632 콘텐츠 업로드
- 대여 가격 안내 문구 60% -> 70%로 수정
2024-11-08 00:39:22 +09:00
b89e563023 라이브방 하트 메시지
- 글자색과 배경색 변경
2024-11-07 16:30:09 +09:00
5a37ba8be0 라이브방
- 룰렛설정, 룰렛, 후원 버튼 터치시 키보드가 내려가도록 설정
2024-10-30 15:21:59 +09:00
37d47efe2c 라이브 후원 - 비밀후원 체크박스
- 체크박스가 선택된 이미지로 변경되지 않던 버그 수정
2024-10-30 12:02:31 +09:00
f6c5be24d8 콘텐츠, 커뮤니티 댓글 입력창
- 길게 입력하면 줄바꿈 되어 화면 내에서 입력한 글이 모두 보이도록 수정
2024-10-29 14:44:19 +09:00
0c8241fba7 콘텐츠 대여 가격
- 소장가격의 60%에서 70% 표기로 변경
2024-10-29 14:33:40 +09:00
dc2cda58b2 라이브
- 우측 하단 옵션 버튼 순서 변경
2024-10-29 13:54:36 +09:00
936074081c 라이브 후원
- 비밀후원을 체크하면 힌트 메시지에 '비밀' 추가
- 후원메시지 최대 길이 50 -> 200 변경
2024-10-29 00:30:34 +09:00
a556378ffe 라이브방 - 라이브 후원랭킹 리스트
- 방장은 일반후원 / 비밀후원 캔을 나눠서 보이도록 수정
2024-10-28 23:49:30 +09:00
960dda8d40 콘텐츠 상세 - 콘텐츠 설명글
- textSize: 14
- color: 909090
- font: gmarket_sans_medium
2024-10-28 13:54:13 +09:00
a401a2e13a versionCode 120, versionName 1.22.2 2024-10-28 13:47:19 +09:00
f51f7ef412 라이브방
- 하트 알림 바(채팅) 배경, 닉네임, 글자색 변경
- 하트 알림 바 위치 수정 : 채팅 -> 공지 밑
2024-10-27 23:36:28 +09:00
04151168ca 라이브방
- 채팅창 너비 화면 가득 채움
2024-10-27 21:51:13 +09:00
99a93001bc 알람 권한 설정
- 사용 하지 않는 권한을 런타임 권한으로 물어 문제가 생기는 버그 수정
2024-10-27 18:28:20 +09:00
e6339bb4c2 개수 제한 콘텐츠
- 내 콘텐츠는 sold out 되더라도 재생이 가능하도록 수정
2024-10-24 23:52:50 +09:00
7209f972d2 라이브 방 - 채팅 EditText
- inputType에서 textMultiLine를 제거하여 imeOptions의 actionSend설정이 적용되도록 수정
2024-10-24 01:45:47 +09:00
0714918338 라이브 방 - 하트 후원 채팅
- 채팅 배경색 변경
2024-10-23 01:39:03 +09:00
94d581a4f3 라이브 방 - 하트 후원 애니메이션
- 룰렛의 활성화/비활성화시 키보드를 숨기고 하트 후원 애니메이션 시작 위치 재계산
2024-10-22 22:16:13 +09:00
577e864b6a 라이브 방 - 하트 후원 애니메이션
- 스피커/리스너 변경시 키보드를 숨기고 하트 후원 애니메이션 시작 위치 재계산
2024-10-22 21:43:56 +09:00
96a3ef44f6 라이브 방
- 하트 후원시 채팅으로 알림
2024-10-17 15:01:05 +09:00
3a33153361 라이브 방
- 하트 후원 안내 팝업 추가
2024-10-16 19:08:39 +09:00
ad0c18dceb 라이브 방
- 하트 총 개수 조회 기능 추가
2024-10-16 18:31:14 +09:00
e964679154 라이브 방
- 하트 후원 API 연결
- 하트 후원 성공시 하트 애니메이션 호출
2024-10-16 18:06:38 +09:00
6c9ace146d 라이브 방
- 하트 애니메이션 추가
2024-10-16 15:54:08 +09:00
c7409e4dec 라이브 방
- 채팅창 너비 축소
- 오른쪽 하단 옵션 버튼 baseline이 채팅창 baseline과 동일하게 설정
- 좋아요(누르면 1캔 후원) 버튼 추가
- 좋아요 개수 UI - 후원 캔 왼쪽에 추가
2024-10-16 02:25:55 +09:00
a4ff89cec0 사용하지 않는 아이콘 제거 2024-10-15 23:54:52 +09:00
b489f46910 푸시 알림 아이콘 - 앱 로고 아이콘으로 변경 2024-10-15 23:53:31 +09:00
4a167a00bd 권한 수정 2024-10-15 19:02:10 +09:00
2aca7620e7 권한 수정 2024-10-14 17:51:43 +09:00
1f7f3bfdb1 큐레이션 콘텐츠
- 남성향이면 여성 크리에이터, 여성향이면 남성 크리에이터 작품만 조회되도록 수정
2024-10-14 02:21:39 +09:00
90ff8ceb72 콘텐츠 메인 - 추천시리즈, 모닝콜, 숏플, 라이브 다시보기
- 남성향이면 여성 크리에이터, 여성향이면 남성 크리에이터 작품만 조회되도록 수정
2024-10-14 01:34:35 +09:00
e8b69cc6b9 가이드 이미지 변경 2024-10-13 22:04:35 +09:00
9143e74a72 소다로 살다 -> 보이스 모닝콜 등록 변경 2024-10-13 21:55:51 +09:00
7bb6e2ae45 스플래시 이미지
- 글자 크기 변경
2024-10-13 21:21:02 +09:00
6f4f500aec 소다라이브 -> 보이스온 2024-10-12 01:17:48 +09:00
d328bdb4d1 스플래시 변경 2024-10-12 01:03:26 +09:00
7cb3d19e85 콘텐츠 전체 보기
- 콘텐츠 제목 영역과 가격 표시 영역이 겹치지 않도록 수정
2024-10-11 19:40:25 +09:00
7c320b7f23 콘텐츠, 라이브 메인
- 보이스 모닝콜 메뉴 추가
- 라이브 다시듣기 메뉴 라이브 메인으로 이동
2024-10-11 14:11:56 +09:00
f2cca1e14b 설정
- 19금 콘텐츠 보기 설정 UI depth 추가
- 콘텐츠 보기 설정으로 제목 변경
2024-10-10 15:42:19 +09:00
f3553c3c59 라이브, 콘텐츠 메인 - 새로운 콘텐츠, 큐레이션
- 민감한 콘텐츠(19금) 설정 추가
2024-10-10 13:36:33 +09:00
5daddc5fef 설정
- 19금 콘텐츠 보기 설정 UI 추가
2024-10-10 13:10:40 +09:00
8cee9fb019 라이브 메뉴 설정 페이지 추가 2024-10-08 16:13:30 +09:00
38e4122570 크리에이터 채널
- 메뉴 설정 버튼 추가
2024-10-07 23:41:59 +09:00
3c178dbb96 룰렛 설정 완료 메시지
- 채널에서 새 룰렛을 저장할 때 성공메시지 수정
2024-10-04 13:58:40 +09:00
4537a95d2d 불필요한 파일 삭제 2024-10-03 00:50:50 +09:00
fd4dfc2dff 시리즈 소개
- 연재 요일 표시에서 마지막에 있는 요일 제거
2024-10-02 18:06:04 +09:00
2d9ec631aa 스플래시 2024년 10월 2024-10-02 15:23:27 +09:00
b59172d608 라이브 방 - 시그니처 이미지
- 로딩이 완료된 시점부터 이미지 표시 시간을 계산하여 최대한 오차가 없도록 조정
2024-09-30 22:03:49 +09:00
790e42035f 크리에이터 채널
- 룰렛 설정 메뉴 추가
2024-09-30 21:10:35 +09:00
7782fe389e 라이브 수정
- 테두리 색 변경
2024-09-25 14:52:56 +09:00
84b64d3283 라이프 참여자 리스트
- 스피커 표시 최대 5명
2024-09-24 19:39:32 +09:00
66a83a118f 콘텐츠 메인
- 배너 indicator 색상 변경
2024-09-24 14:08:30 +09:00
3494ff0491 콘텐츠 상세
- 시간표시 슬라이더 색상변경
2024-09-24 14:01:50 +09:00
d51fc88813 콘텐츠 업로드
- 미리듣기 최소 시간 15초로 변경
2024-09-24 13:47:14 +09:00
1930014498 라이브 비밀후원
- 방장인 경우 비밀후원도 후원 총액에 반영되도록 수정
2024-09-24 12:14:47 +09:00
4189ee4e54 라이브
- 큰 음소거 이미지 변경
2024-09-20 16:12:13 +09:00
a191e295da 라이브
- 최대 스피커 수 방장 포함 6명으로 변경
2024-09-20 16:10:48 +09:00
0709c68653 크리에이터 채널
- 공유버튼 변경
2024-09-20 15:34:00 +09:00
084c306159 시리즈 상세 - 크리에이터 팔로우와 알림설정
- 팔로잉 상태에서 알림 켜기/끄기 상태 추가
2024-09-20 12:11:33 +09:00
2ed77a3332 콘텐츠 상세 - 크리에이터 팔로우와 알림설정
- 팔로잉 상태에서 알림 켜기/끄기 상태 추가
2024-09-20 11:44:13 +09:00
5b2c5a6e2f 팔로잉/팔로워 리스트 - 팔로우와 알림설정
- 팔로잉 상태에서 알림 켜기/끄기 상태 추가
2024-09-20 01:57:27 +09:00
5063ce815d 크리에이터 채널 - 팔로우와 알림설정
- 팔로잉 상태에서 알림 켜기/끄기 상태 추가
2024-09-13 16:11:39 +09:00
78323103fd 라이브 정보 수정
- 연령 제한 설정 추가
2024-09-11 23:26:45 +09:00
772005910c 시리즈 콘텐츠 리스트
- 정렬(최신순, 등록순) 추가
2024-09-10 16:27:32 +09:00
1f36d8ef25 커뮤니티 댓글, 팬토크, 콘텐츠 댓글
- 프로필 이미지 터치시 차단, 신고가 가능한 유저 프로필 표시
2024-09-07 01:28:50 +09:00
1b8d65c3ea 라이브 방 - 유저 차단 팝업
- 리스너가 차단할 때 팝업 문구 수정
2024-09-05 20:01:16 +09:00
07df9ced75 PG 결제 - 카카오페이 결제
- 구매자 정보 추가
2024-09-05 16:07:27 +09:00
9dfd73b090 라이브 방
- 차단한 유저의 채팅이 보이지 않도록 수정
2024-09-04 22:29:55 +09:00
0ebb34a2df 차단 리스트 페이지 추가 2024-09-04 12:31:32 +09:00
6869cd62ea 팔로잉 리스트
- 팔로우 한 채널이 없는 경우 '팔로우 중인 채널이 없습니다.' 표시
2024-09-02 18:01:18 +09:00
b7b73bb409 마이 페이지
- 팔로잉 리스트 버튼 추가
2024-09-02 17:47:28 +09:00
7bf3728cf9 커뮤니티 댓글
- 비밀 댓글 체크박스 보이지 않도록 수정
2024-09-02 14:32:12 +09:00
cc01312ae5 콘텐츠 댓글
- 일반 댓글의 닉네임이 잘리는 버그 수정
2024-08-30 19:00:35 +09:00
cf94615e71 라이브 방 비밀후원, 콘텐츠 비밀댓글
- 체크박스 네모로 변경
2024-08-30 18:17:06 +09:00
51cb36b2e6 라이브 방
- 후원현황과 후원하기 버튼 순서 변경
2024-08-30 15:07:35 +09:00
345df7a7b7 콘텐츠 댓글 리스트
- 비밀댓글은 닉네임 옆에 '비밀댓글' 마크 추가
2024-08-30 15:03:46 +09:00
4961727237 콘텐츠 댓글 리스트
- 댓글이 없을 때 유료 콘텐츠를 구매한 사람이 비밀댓글을 등록할 수 있는 기능 추가
2024-08-30 14:42:20 +09:00
6640130ef9 콘텐츠 상세
- 댓글이 없을 때 유료 콘텐츠를 구매한 사람이 비밀댓글을 등록할 수 있는 기능 추가
2024-08-30 13:11:56 +09:00
e38778d9e7 캔 결제수단 추가
- 카카오페이
2024-08-27 19:33:58 +09:00
63e9b705a9 스플래시 2024/09 2024-08-27 00:06:41 +09:00
38864ca666 포그라운드 서비스 시작시 foregroundServiceTypes 추가 2024-08-26 15:24:45 +09:00
adab4aee48 라이브 후원
- 비밀 후원 기능 추가
2024-08-24 00:37:53 +09:00
3bad5c5e55 콘텐츠 메인
- 우측 최상단에 콘텐츠 보관함, 알람 아이콘 추가
- 배너와 추천 시리즈 순서 변경
2024-08-21 21:49:42 +09:00
7607c10bdc @Keep 어노테이션을 추가하여 난독화에서 제외되도록 수정 2024-08-21 20:22:39 +09:00
4349f2bd3a targetSdk 34로 변경
커스텀 액션에서 패키지를 명시
2024-08-21 12:44:51 +09:00
62abd3c900 라이브
- 후원 메시지, 룰렛 결과 모든 유저에게 보이도록 수정
2024-08-14 18:34:39 +09:00
63193c82a1 versionCode 96, versionName 1.15.1 2024-08-12 17:22:45 +09:00
6431577bf1 알람
- 날짜 선택 추가
2024-08-12 16:58:23 +09:00
b56a0d58bf 알람
- 볼륨 설정 추가
2024-08-09 15:22:17 +09:00
00774739ed 팬토크
- 응원글 등록/수정/삭제시 전체 데이터를 지우고 다시 조회하도록 수정
2024-08-08 02:19:26 +09:00
b98cc8ed79 크리에이터 커뮤니티 오디오 녹음
- 파일명 수정
2024-08-08 02:02:21 +09:00
81faf3f7ee 크리에이터 커뮤니티 게시글 오디오 재생
- 크리에이터 커뮤니티 게시글 오디오 재생시 백그라운드에서 재생 중인 오디오 콘텐츠 일시정지
2024-08-06 12:14:15 +09:00
14c72e8259 크리에이터 커뮤니티 게시글 전체리스트
- 오디오 콘텐츠 재생기능 추가
2024-08-06 00:11:35 +09:00
94d719a814 크리에이터 커뮤니티 게시글 등록- 오디오 녹음 다이얼로그
- x버튼을 눌렀을 때 올리지 않을 파일을 삭제하는 로직 추가
- 드래그 해서 화면이 꺼지지 않도록 수정
2024-08-05 21:31:03 +09:00
66fecf1509 크리에이터 커뮤니티 게시글 등록- 오디오 녹음
- 녹음 시간 : centiseconds 까지 표시
- 음질 조정 - sampleRate: 48k, bitrate: 256k
2024-08-05 17:54:18 +09:00
e6e3df701d 크리에이터 커뮤니티 게시글 등록
- 오디오 녹음 기능 추가
2024-08-02 18:03:56 +09:00
76aaaddb5a 음성메시지
- 속닥 -> 메시지
- 다시 녹음 버튼 글자색 #3bb9f1
2024-08-02 17:05:28 +09:00
fa23263b19 온보딩 이미지 변경 2024-07-31 23:32:10 +09:00
2ba2014f82 알람 설정
- 처음 설정시 알람 id값을 받아오지 않아 알람이 울리지 않던 버그 id값을 받아오도록 수정
2024-07-31 22:08:31 +09:00
ee6090c103 알람 권한 요청 코드 추가 2024-07-31 19:59:48 +09:00
063158e9e0 회사정보 고객센터 이용시간 추가 2024-07-31 18:30:20 +09:00
a1ac80c180 versionCode 90, versionName 1.14.0 2024-07-31 12:10:47 +09:00
42a8c43ff5 앱 가이드 이미지 변경 2024-07-30 22:46:00 +09:00
1fcd3cc309 앱 아이콘 변경 2024-07-30 22:16:56 +09:00
66cdb621ca 스플래시 변경 2024-07-30 21:34:15 +09:00
a618903f0f 알람
일회성 알람의 경우 알람이 울리면 날짜를 다음날로 변경하고 isEnable을 false로 변경한다.
2024-07-29 12:20:58 +09:00
4fc39f10b1 알람
setExactAndAllowWhileIdle -> setAlarmClock 로 변경
2024-07-29 11:42:52 +09:00
b393ddcd74 label 수정
알람 -> 소다로 살다
2024-07-26 16:05:23 +09:00
7aa80154fa label 수정
알람 -> 소다로 살다
2024-07-26 15:03:09 +09:00
262f42c194 PG 심사를 위해
- 소장 기간을 PG심사용 계정이면 (이용기간 1년)으로 나오도록 수정
2024-07-26 14:45:01 +09:00
18d16a2211 sdk 31이상에서 PendingIntent.FLAG_IMMUTABLE FLAG 추가 2024-07-26 01:43:00 +09:00
7c91795fb5 알람
- 추가 슬롯 구매하기 기능 추가
2024-07-26 00:52:34 +09:00
856e13d37b 알람
- 알람음 -> 콘텐츠로 변경
2024-07-25 18:21:33 +09:00
2223919a98 알람
- 소장한 콘텐츠만 알람으로 지정 가능하다는 안내문구 추가
2024-07-25 18:20:24 +09:00
7b4063420e 알람
- 최대 3개까지 등록되도록 수정
2024-07-25 16:24:43 +09:00
839ff7463e 알람 기능 추가 2024-07-25 16:10:38 +09:00
7587b8bc25 PG 결제 - 휴대폰
- 헥토파이낸셜로 변경
2024-07-19 15:06:38 +09:00
756675e622 versionCode 82 2024-07-05 21:18:47 +09:00
2f059cc783 룰렛 합계 검증
- Float 값이라 정확히 100.0이 나오지 않으므로 totalPercentage가 99.9보다 크고 100.1보다 작으면 통과되도록 조건 수정
2024-07-02 21:32:23 +09:00
573d5a89f9 회원가입
- 개인정보 처리방침 터치시 이용약관이 나오는 버그 수정
2024-07-02 18:39:30 +09:00
40472bdced PG 수정 - payment method
- 디지털 카드 -> 카드
- 디지털 계좌이체 -> 계좌이체
2024-07-02 14:49:20 +09:00
ac7124a7e6 PG 수정
- 헥토 검증 API 추가
2024-06-29 18:59:51 +09:00
888674b776 PG 수정
- 휴대폰 결제: 웰컴페이먼츠
- 나머지 : 헥토파이낸스(세틀뱅크)
2024-06-29 18:33:30 +09:00
c9f6088754 라이브 방
- 공유하기 추가
2024-06-27 19:28:49 +09:00
687aada611 콘텐츠 업로드
- 한정판 업로드 추가
2024-06-18 16:13:56 +09:00
20a1e8f1d7 versionCode "80"
versionName "1.12.3"
2024-06-05 19:16:04 +09:00
357d6c3406 크리에이터 커뮤니티
- 커뮤니티 글이 보이지 않던 버그 수정
2024-06-05 19:13:12 +09:00
40ff959289 콘텐츠 리스트
- 소장중 / 대여중 / Sold Out 표시
2024-06-04 19:56:46 +09:00
22825d1520 회사정보 주소 수정 2024-05-30 13:26:40 +09:00
827a8258ec 크리에이터 커뮤니티 게시물
- 구매하지 않은 유료 게시물도 내용은 보이도록 수정
2024-05-30 13:24:40 +09:00
8505be882c 스플래시 06월 2024-05-28 16:23:28 +09:00
d0eb4103ba 크리에이터 커뮤니티
- 유료 & 구매하지 않은 게시글만 이미지가 Blur 처리 되도록 수정
2024-05-28 12:23:54 +09:00
ba83027dc9 회사정보 대표 이메일 추가 2024-05-25 00:09:40 +09:00
7b3fa0b224 콘텐츠 메인 탭
- 하단에 회사정보 추가
2024-05-24 23:38:16 +09:00
eb7f83433c 마이페이지 탭
- PG심사용 계정의 경우 캔 표시 영역 뷰 제거
2024-05-24 00:19:24 +09:00
9d5ca7c36d 커뮤니티 유료 게시글 조회, 구매 기능 추가 2024-05-23 23:13:25 +09:00
a9511dcb51 커뮤니티 게시글 등록
- 유료 게시글 등록은 이미지가 있어야 등록이 가능하도록 수정
2024-05-23 01:27:29 +09:00
5d42e2b16e 커뮤니티 게시글 등록
- 유료 게시글 등록을 위해 가격 설정 추가
2024-05-23 00:05:35 +09:00
6fac6ecd4d 확률을 입력하지 않아서 crash 나는 버그 수정 2024-05-22 19:33:15 +09:00
b876695adc 회사정보 변경 2024-05-21 01:25:17 +09:00
b220b8bce4 콘텐츠 상세, 콘텐츠 구매
- pg 테스트 계정의 경우 캔이 아닌 원으로 표시되도록 하고 콘텐츠 구매시 바로 결제 후 구매 되도록 수정
2024-05-20 15:37:46 +09:00
ba4d707d45 9970ff -> 3bb9f1 2024-05-17 15:35:26 +09:00
2920d2a7ae 9970ff -> 3bb9f1 2024-05-17 15:28:59 +09:00
b8abef6aeb 9970ff -> 3bb9f1 2024-05-17 15:11:41 +09:00
75fc34cbe3 9970ff -> 3bb9f1 2024-05-17 15:09:47 +09:00
206fc398c6 라이브, 회원 태그
- 9970ff -> 3bb9f1 색상 변경
2024-05-16 14:23:07 +09:00
b95b77bcb9 룰렛 설정
- 옵션 확률 총합 추가
2024-05-16 12:41:20 +09:00
62c269cac2 룰렛 설정
- 옵션 추가시 확률이 빈칸으로 표시되어 NumberFormat Exception이 나오던 버그 예외처리
2024-05-16 12:11:05 +09:00
4b3474ff42 라이브 방 생성
- 크리에이터 입장 가능 설정 추가
2024-05-14 17:06:12 +09:00
d6e9b929e9 라이브 방
- 음소거 버튼 위치 아래로 이동
2024-05-11 04:00:26 +09:00
13efce42a0 룰렛 설정
- 룰렛 1, 2, 3 버튼 bg, text 색상 변경
2024-05-11 03:55:58 +09:00
85ccc18485 룰렛 변경
- 확률 수동 설정
- 여러개의 룰렛이 켜져있을 때 선택하여 돌리기
- 후원 히스토리에 룰렛 히스토리 추가
2024-05-10 17:55:20 +09:00
9df08cdf24 콘텐츠 메인
- 새로운 콘텐츠 아래 새로고침 버튼 제거
2024-05-08 12:17:28 +09:00
7892eed443 라이브
- 라이브가 없을 때 문구 수정
- 라이브가 없을 떄 문구 자간 및 폰트 사이즈 수정
2024-05-08 12:15:13 +09:00
eca68c06c3 라이브 메인 - 라이브 없을 때 문구
- 🙀마이페이지에서 본인인증을 하거나 라이브를 예약하고 참여해보세요.
2024-05-07 19:27:34 +09:00
3c82ff1c4e 라이브 메인
- 당겨서 새로고침 제거
- 새로고침 버튼 추가
2024-05-07 19:02:58 +09:00
8c6aff1623 콘텐츠 메인 - 추천 시리즈, 새로운 콘텐츠
- 새로고침 버튼 추가
2024-05-07 18:58:15 +09:00
254a1e3381 콘텐츠 메인
- 추천 시리즈 UI 추가
2024-05-07 16:42:14 +09:00
dff4c833f1 . 2024-05-04 02:47:03 +09:00
5fa3a591d4 인 앱 결제
- 서버 호출 후 충전이 완료되면 충전완료 페이지로 이동하도록 수정
2024-05-03 19:47:19 +09:00
845578a1dd 시리즈 아이템
- 커버이미지 DIM 제거
2024-05-03 13:41:30 +09:00
84ac72b391 탐색
- 크리에이터가 없으면 섹션제거
2024-05-03 13:36:41 +09:00
db364d9bf7 라이브
- 시그니처 ON/OFF 버튼 추가
- 공유하기 버튼 제거
2024-05-02 15:17:00 +09:00
3fe01f8def 2024년 5월 인트로 적용 2024-05-01 22:44:16 +09:00
02c815077e 시그니처 후원
- 시그니처 별로 설정된 시간 만큼 GIF가 재생되도록 기능 추가
2024-05-01 22:14:58 +09:00
108eb759ec 시리즈 전체보기
아이템 세로 간격 수정
2024-05-01 00:39:17 +09:00
fef49a0d6a 시리즈 전체보기
아이템 사이즈 수정
2024-04-30 22:33:37 +09:00
87241fa8bd 시그니처 후원
- 위치 가운데로 수정
2024-04-30 19:55:47 +09:00
29d5192fff 시리즈 콘텐츠
- 대여중/소장중/가격 뱃지가 동시에 표시되는 버그 수정
2024-04-30 19:28:16 +09:00
c86e55719e 인 앱 결제
- 사용하지 않는 함수 제거
2024-04-30 17:18:19 +09:00
839a8a780c 시리즈 상세 작품소개
- 0원 -> 무료로 변경
2024-04-30 14:57:32 +09:00
346334a0ba 시리즈 상세 작품소개
- 구분선 색 변경
2024-04-30 13:32:10 +09:00
7cd4d180c2 시리즈 상세 콘텐츠
- 제목이 가격영역을 침범하는 버그 수정
2024-04-29 11:47:24 +09:00
320ef4fbc7 시리즈 상세
- 19금과 전체연령가가 반대로 표시되던 버그 수정
2024-04-29 11:41:13 +09:00
b4623141f3 크리에이터 채널
- 시리즈 데이터가 파싱되지 않던 버그 수정
2024-04-27 04:23:38 +09:00
30d3ed14d7 시리즈 콘텐츠 리스트 아이템
- 출시 날짜 제거
- 재생 시간 제목 윗쪽으로 이동
2024-04-27 03:14:09 +09:00
ae617d5154 크리에이터 채널 시리즈
- 이미지 사이즈
가로 102 -> 116.7, 세로 144 -> 165 변경
2024-04-27 03:10:51 +09:00
9dd3c568d8 시리즈 상세
- 스크롤시 타이틀 윗쪽에 배경이 살짝 보이던 버그 수정
2024-04-27 02:54:39 +09:00
f31fc7691e 시리즈 상세
- 내용 스크롤 시 뒤로가기 버튼도 같이 스크롤 되도록 수정
2024-04-27 02:01:53 +09:00
27df922383 시리즈 상세 작품소개
- 키워드 세로 간격 수정
- 상세정보 글자색 흰색으로 변경
2024-04-27 01:57:54 +09:00
d5956c024d 시리즈 상세
- 팔로잉 액션 추가
2024-04-27 01:25:02 +09:00
29aca74651 시리즈 상세
- 전체회차 보기 사이즈 16으로 변경
- 태그 글자 크기 12로 변경
2024-04-27 01:08:22 +09:00
f41790b302 시리즈 콘텐츠 전체보기
- 제목 - 전체회차 듣기 로 변경
2024-04-27 00:47:44 +09:00
0b999a874c 시리즈 콘텐츠 전체보기 페이지 추가 2024-04-27 00:34:42 +09:00
2778638dc9 시리즈 상세보기
- 콘텐츠 링크 추가
2024-04-26 23:27:26 +09:00
cc5fe445fc 시리즈 상세보기 페이지 추가 2024-04-26 23:17:44 +09:00
c310f9c57e 시리즈 전체보기 페이지 추가 2024-04-25 22:05:58 +09:00
cd607425a0 크리에이터 채널
- 시리즈 section 추가
2024-04-25 17:14:19 +09:00
a5df8a1110 크리에이터 채널
- 활동요약표 선 색깔 button색으로 수정
2024-04-22 15:05:16 +09:00
2bd30aa346 versionCode 57, versionName 1.9.11
인 앱 결제 로직 수정
- 결제 완료 후 서버에서 데이터 처리 후 로컬에서 다시 소비처리를 하도록 수정
2024-04-22 14:40:16 +09:00
a6ce994fd0 versionCode 49, versionName 1.9.2 2024-04-12 19:57:44 +09:00
7bffd1c3c7 후원하기 팝업
- 캔 충전하기 페이지로 이동하는 버튼 직관적으로 보이도록 화살표에서 충전 글자로 변경
2024-04-12 14:30:30 +09:00
dfd92d6db6 음소거 버튼 위치 - top
크리에이터 팔로우 버튼 margin_top = 5.3
시그니처 후원 움짤 위치 - 우측 중간으로 이동
2024-04-12 14:09:47 +09:00
5529872bd5 본인인증을 하지 않아도 PG결제가 보이도록 수정 2024-04-05 11:46:18 +09:00
364a530956 크리에이터 커뮤니티 대댓글
- 부모 댓글과 동일한 글자 색상으로 변경
2024-04-02 15:36:49 +09:00
0556d5a067 오픈 예정 콘텐츠 상세
- 댓글 창, 좋아요, 공유, 후원 버튼 숨김
2024-04-02 14:47:56 +09:00
be46893555 캔 충전 페이지
- PG, IAP 순서로 탭 변경
2024-04-02 00:33:12 +09:00
f7f789892d okhttp connect, read, write timeout 시간 60초 설정 2024-04-01 16:38:58 +09:00
de0d327168 인트로
- 4월 인트로로 변경
2024-04-01 15:48:06 +09:00
e52075a692 라이브
- 팔로우/팔로잉 버튼 변경
2024-03-28 02:01:24 +09:00
a29c50eae3 콘텐츠 상세
- 한정판이 매진된 경우 Player 가운데 Sold Out 표시
2024-03-28 01:35:21 +09:00
8a2a497fcf 콘텐츠 상세
- 한정판이 매진된 경우 구매버튼 비활성화 및 '해당 콘텐츠가 매진되었습니다.' 문구 표시
2024-03-27 18:59:28 +09:00
6786988e63 콘텐츠 상세
- 한정판이 매진된 경우 구매버튼 비활성화 및 '해당 콘텐츠가 매진되었습니다.' 문구 표시
2024-03-27 16:59:29 +09:00
2dd5e2d96c 콘텐츠 상세
- 한정판인 경우 대여/소장 선택 다이얼로그를 생략하고 바로 구매확인 다이얼로그가 나오도록 수정
2024-03-27 16:40:15 +09:00
03320d9cec 콘텐츠 상세
- 한정판 UI 추가
2024-03-27 00:04:28 +09:00
2101cdeb86 인 앱 결제 추가 2024-03-25 17:40:17 +09:00
f0a8ca5823 Toast show 로직 수정 2024-03-23 06:05:08 +09:00
daed389264 인 앱 결제 로직 수정
versionName 1.8.16, versionCode 41
2024-03-23 05:37:25 +09:00
6a558ad25c versionName 1.8.7, versionCode 32 2024-03-22 12:07:56 +09:00
6e3a4e1125 캔 충전페이지
- 인 앱 결제 페이지 추가
2024-03-21 04:14:52 +09:00
79cb4b995a 캔 충전페이지
- 인 앱 결제 추가를 위해 단일 액티비티에서 탭 구성으로 변경
2024-03-20 01:24:39 +09:00
84b9dd0841 versionCode 27 versionName "1.8.2" 2024-03-15 18:05:52 +09:00
4004dcd99e 라이브 메인 지금 라이브 중
- 이미지 크기 가로 116 -> 128, 세로 164 -> 179
2024-03-15 00:49:11 +09:00
ebbe7e8917 라이브 메인 지금 라이브 중, 라이브 전체 보기
- 이미지 크기 수정, 잠금 아이콘 사이즈 40 -> 60
2024-03-14 23:51:37 +09:00
196d5c6cfd 라이브 메인 - 지금 라이브 중
- 입장 가능한 잔여 인원 표시 제거
2024-03-14 23:20:00 +09:00
f4626a7cd5 라이브 - 호스트는 리스너로 변경 버튼이 보이지 않도록 수정 2024-03-14 22:57:04 +09:00
4ed97d4600 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 19:13:21 +09:00
c2cf05ef64 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 18:46:22 +09:00
0e131f6661 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 18:27:25 +09:00
d091c47a0c 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 18:01:45 +09:00
6bd5d26882 지금 라이브 중 전체보기 - UI 표시 방식 수정
- 1줄에 1개 보이던 리스트 방식에서 1줄에 3개씩 표시하는 그리드 방식으로 변경
2024-03-14 17:37:05 +09:00
e02ea116ff 지금 라이브 중 - 참여 가능 인원 표시 2024-03-14 17:22:37 +09:00
012d1d94d5 지금 라이브 중 - 라이브 아이템 유/무료 표시 방식 수정
- 무료 배경색 : #111111
- 유료 배경색 : #DD4500
- 유료 표시 - white 캔과 함께 100 과 같이 가격으로 변경
2024-03-14 16:50:57 +09:00
39e49b08d9 본인이 스피커일 때 리스너로 변경하는 버튼 추가 2024-03-14 14:48:43 +09:00
2a84d2dc41 시그니처 재생 길이 수정
- 3초에서 7초로 변경
2024-03-14 14:41:21 +09:00
66c9b38e04 versionCode 26 versionName "1.8.1" 2024-03-11 12:24:44 +09:00
8f35f7a573 라이브 정보 수정
- 메뉴 설정을 하지 않고 다른 라이브 정보만 수정 했을 경우 앱이 종료 되는 버그 수정
(selectedMenu가 초기화 되지 않아도 라이브 정보를 수정할 수 있도록 수정)
2024-03-11 12:16:49 +09:00
c653563512 라이브 만들기
- 메뉴 등록 edittext 스크롤 적용
2024-03-08 23:04:54 +09:00
a75821ed06 스플래시 화면 변경 2024-03-08 21:04:51 +09:00
ca6416c697 라이브 정보 변경 다이얼로그
- 공지, 메뉴 입력 창 스크롤 적용
2024-03-08 04:55:49 +09:00
f7b3caf320 스플래시 화면 변경 2024-03-08 04:49:34 +09:00
62d839b69b 시그니처 후원 이미지 표시
- gif만 표시되던 것 모든 이미지로 변경
2024-03-08 04:47:47 +09:00
a9c3ea953d 시그니처 후원 이미지 표시
- 이미지 URL을 배열에 저장 후 순서대로 표시하도록 수정
2024-03-08 02:46:25 +09:00
5be9720fcc 시그니처 후원 이미지 표시
- 하단 마진 20 -> 65로 변경
2024-03-08 01:37:55 +09:00
ec096b5831 후원
- 시그니처 후원 적용
2024-03-08 00:51:43 +09:00
51c5e5f32c 에러가 아닌데 에러로 표시하는 부분 warning으로 변경 2024-03-07 06:33:10 +09:00
b6d7e0b0e9 불필요한 로그 제거 2024-03-07 06:32:22 +09:00
49209c4c4a 라이브 정보 수정
- 메뉴판 수정 기능 추가
2024-03-07 06:31:25 +09:00
6b466ac7d7 라이브 메인 지금 라이브 중
- 가격 유/무료 뱃지 배경색 3bb9f1(버튼색)으로 변경
2024-03-07 02:36:05 +09:00
d6182f9e03 라이브 메인 추천 채널
- 라이브 중 표시 컬러 9970ff -> 3bb9f1로 변경
2024-03-07 02:33:28 +09:00
2e24a298ff 라이브 만들기
- 메뉴판 설정 추가
2024-03-07 02:29:38 +09:00
d7d43bc7be 라이브
- 메뉴판 UI 추가
2024-03-06 17:02:37 +09:00
2d0c4ea738 룰렛 설정 개수에 따라 룰렛 프리셋 버튼 활성화/비활성화 2024-02-28 03:56:51 +09:00
af4e802259 룰렛 설정 문구 수정 2024-02-28 03:39:42 +09:00
ad9e97161c 커뮤니티 댓글, 콘텐츠 댓글, 팬토크
- 여러줄 인 경우 줄간격 수정
2024-02-28 02:25:44 +09:00
1712f509dc gaid 업데이트 추가 2024-02-26 21:56:10 +09:00
78dd3b2785 커뮤니티 댓글, 콘텐츠 댓글, 팬토크
- 글자 크기, 색상 수정
2024-02-24 23:30:56 +09:00
f7f4638526 룰렛 프리셋 설정
- 변동 사항이 있을 경우에만 업데이트 하도록 수정
2024-02-23 18:39:42 +09:00
66690a6f89 룰렛 프리셋 설정 성공 메시지
- 현재 선택된 룰렛 프리셋의 활성화/비활성화에 따라 성공 메시지가 수정되도록 수정
2024-02-23 17:57:49 +09:00
4e56eb9bde 룰렛 프리셋 적용 2024-02-23 14:07:52 +09:00
e3349e41a1 versionCode 23 versionName "1.6.1" 2024-02-16 14:10:34 +09:00
239ccb9018 튜토리얼 이미지 변경 2024-02-16 14:09:49 +09:00
6ff17e1ba6 콘텐츠 그리드 리스트
- 아이템 상하 간격 16으로 변경
2024-02-15 00:58:52 +09:00
1c88a90024 튜토리얼 이미지 순서 변경 2024-02-15 00:55:41 +09:00
dd54e5b97a 튜토리얼 수정 2024-02-14 23:59:28 +09:00
f4180eec14 콘텐츠 메인 숏플, 라이브 다시 듣기 버튼 수정
- 부가 설명 제거
- 버튼 배경 이미지로 변경
- 글자 크기 14.7 -> 16.7로 변경
- 아이콘 큰 사이즈로 변경
2024-02-14 23:39:53 +09:00
b81576bdaf 스플래시 화면 이미지 변경 2024-02-14 23:30:40 +09:00
bf43926d7d 라이브 방
- 후원 시 '20캔을' 색을 바뀌던 것을 '20캔'까지만 색이 바뀌도록 수정
2024-02-14 23:12:15 +09:00
1ec0d8540a 콘텐츠 리스트 아이템 - 사이즈, 간격 수정 2024-02-14 18:19:47 +09:00
91a2da9032 versionCode 22 versionName "1.6.0" 2024-02-14 16:49:36 +09:00
a2cbeb87d5 콘텐츠 메인
- 숏플, 라이브 다시 듣기 추가
2024-02-14 02:10:30 +09:00
a88f8c3316 큐레이션
- 아이템 2개에서 3개로 변경
2024-02-13 14:17:57 +09:00
e51af38b75 큐레이션
- 아이템 2개에서 3개로 변경
2024-02-13 02:21:28 +09:00
6a92f955ca versionCode 22 versionName "1.5.2" 2024-02-13 00:35:44 +09:00
a7ef5a8147 라이브 상세
- 크리에이터는 예약자(참여자) 표시
2024-02-13 00:34:34 +09:00
a14263bf27 versionCode 21 versionName "1.5.1" 2024-02-07 20:38:31 +09:00
489b09a54e 콘텐츠 전체보기 카테고리
- 아이템 간격 13.3 -> 8 로 수정
2024-02-07 18:39:58 +09:00
51b57a0b1d 콘텐츠 전체보기
- 카테고리 추가
2024-02-07 07:28:14 +09:00
eafb922ae9 설정페이지
- 회사정보 추가
2024-02-05 22:06:44 +09:00
6cfbdd7acd 콘텐츠 상세
- 미리듣기가 없을 떄 문구 추가
2024-01-29 16:23:38 +09:00
f8fd06706b 크리에이터 콘텐츠 리스트
- 고정 콘텐츠 핀 추가
2024-01-29 00:48:45 +09:00
c23ef771be 콘텐츠 상세
- 콘텐츠 고정/해제 기능 추가
2024-01-29 00:33:50 +09:00
0bbb1e070c 콘텐츠 상세
- 미리듣기 없는 콘텐츠는 재생 버튼이 보이지 않도록 수정
2024-01-26 13:31:51 +09:00
fba11ae4b9 콘텐츠 업로드
- 미리듣기 여부 선택 버튼 추가
2024-01-26 02:43:55 +09:00
6fbb98ca7b 유료방 입장 팝업
- UI 수정
- 알림 문구 수정
2024-01-22 16:09:53 +09:00
b33c8ffd6d 유료방 입장 팝업
- 라이브 시작 시각 -> 시작 시각 으로 변경
2024-01-22 03:19:12 +09:00
14b3bfbae7 유료방 입장 팝업
- 1시간 이상 지난 후 팝업 내용 변경
- 라이브 시작 시각, 현재 시각 표시
2024-01-21 17:34:07 +09:00
7386c93d73 커뮤니티 글 - 줄 간격 0 -> 8 로 변경 2024-01-19 16:16:47 +09:00
4af8d7dce1 라이브 - 배경이미지 가운데 정렬 2024-01-18 18:46:59 +09:00
08d2ccdab4 라이브 - 배경이미지 상단정렬 2024-01-18 18:27:06 +09:00
0a8abeefbe 앱이 켜져 있을 때 딥링크가 동작하지 않던 버그 수정 2024-01-17 03:53:41 +09:00
f90e089f8a 프로필 이미지 변경
- 이미지 변경시 로컬에 저장된 프로필 이미지 URL도 변경 하도록 수정
2024-01-17 03:14:04 +09:00
1eb905fe30 라이브
- 상단 총 받은 캔 사이즈 10->14로 변경
2024-01-16 22:44:14 +09:00
fcc55288b1 라이브
- 오른쪽 하단 버튼 패딩 수정
2024-01-16 22:37:03 +09:00
97c5dc4363 라이브
- 9970ff 색상 3bb9f1로 변경
2024-01-16 22:18:05 +09:00
4223f0bc5d 라이브
- 후원, 입장, 룰렛 결과 채팅 배경색 변경
2024-01-16 21:34:48 +09:00
7ada4515aa 라이브 - 공지 활성화/비활성화 버튼 색 변경 2024-01-16 19:38:46 +09:00
8456f2b30b 라이브 - 19 배경 변경 2024-01-16 18:56:47 +09:00
c20802f89c 라이브 - 공지 UI 수정 2024-01-16 18:20:03 +09:00
804f993b25 라이브 - 아고라 로컬 사용자의 음성 활동 감지를 비활성화 하여 말하는 유저 표시를 좀 더 자연스럽게 수정 2024-01-16 17:24:11 +09:00
869b83804d 크리에이터가 말할 때 크리에이터 배경 표시 2024-01-16 15:59:34 +09:00
9d97feb184 라이브 공지 버튼 수정 2024-01-16 15:53:51 +09:00
42a69609a1 라이브 배경 이미지 사이즈 비율
400:564로 변경
2024-01-16 15:10:15 +09:00
3a541f71a6 라이브 상단 UI 변경 2024-01-16 05:31:23 +09:00
d75e4af348 댓글 쓰기 텍스트 필드 border 색상 변경 2024-01-15 23:38:30 +09:00
2dac54b3ec 크리에이터 커뮤니티
- 링크 적용
2024-01-15 23:36:02 +09:00
82cf1658cb 라이브 방
- 말하는 사람 배경 변경
- 배경 On/Off 색상 변경
- 참여자 숫자 색상 변경, 폰트 Bold
2024-01-15 23:11:08 +09:00
127e1f6673 본인이 스피커 인 경우 말하는 사람 배경이 잘 보이지 않는 버그 수정 2024-01-15 21:05:30 +09:00
22f1e4024e versionName 1.4.0, versionCode 18 2024-01-11 16:04:51 +09:00
b0bfe2ac06 콘텐츠 상세
- 오픈 예정 날짜 데이터가 있더라도 콘텐츠를 올린 크리에이터의 경우 재생버튼이 동작하도록 수정
2024-01-11 00:27:45 +09:00
d67bb8be50 크리에이터 채널, 콘텐츠 상세
- 오픈예정 추가
2024-01-10 01:57:04 +09:00
eeac702cd7 다이얼로그
- 취소 버튼 글자색 변경
2024-01-09 19:46:37 +09:00
c2216ab054 라이브 예약완료
- 홈으로 이동, 예약이 완료되었습니다 string 글자색 변경
2024-01-08 20:23:36 +09:00
45e5494653 콘텐츠 업로드
- 예약 업로드를 위해 날짜/시간 선택 추가
2024-01-08 15:10:30 +09:00
5f7ed22711 라이브 만들기 액티비티
- 요즘라이브 컬러에서 소다라이브 컬러로 변경
2024-01-05 22:15:07 +09:00
49a823895d 콘텐츠 업로드 액티비티
- 요즘라이브 컬러에서 소다라이브 컬러로 변경
2024-01-05 17:38:01 +09:00
92324e3409 커뮤니티
- 이미지가 잘려서 보이는 현상 수정
2024-01-04 15:41:05 +09:00
13057af98a 쿠폰번호 입력 필터 수정
AS-IS : 영대문자와 숫자만 입력되도록 필터적용

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

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

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

123
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

10
.idea/deploymentTargetDropDown.xml generated Normal file
View File

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

18
.idea/deploymentTargetSelector.xml generated Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-10-14T08:13:14.161127Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=2cec640c34017ece" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

10
.idea/migrations.xml generated Normal file
View File

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

View File

@@ -14,7 +14,7 @@ plugins {
android {
namespace 'kr.co.vividnext.sodalive'
compileSdk 33
compileSdk 34
viewBinding {
enabled true
@@ -24,10 +24,6 @@ android {
dataBinding true
}
lintOptions {
checkDependencies true
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
@@ -38,9 +34,9 @@ android {
defaultConfig {
applicationId "kr.co.vividnext.sodalive"
minSdk 23
targetSdk 33
versionCode 3
versionName "1.0.2"
targetSdk 34
versionCode 165
versionName "1.36.0"
}
buildTypes {
@@ -50,19 +46,46 @@ android {
buildConfigField 'String', 'BASE_URL', '"https://api.sodalive.net"'
buildConfigField 'String', 'BOOTPAY_APP_ID', '"64c35be1d25985001dc50c87"'
buildConfigField 'String', 'BOOTPAY_APP_HECTO_ID', '"664c1707b18b225deca4b429"'
buildConfigField 'String', 'AGORA_APP_ID', '"e34e40046e9847baba3adfe2b8ffb4f6"'
buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"15cadeea4ba94ff7b091c9a10f4bf4a6"'
buildConfigField 'String', 'NOTIFLY_PROJECT_ID', '"765102ec85855aa680da35f1b0f55712"'
buildConfigField 'String', 'NOTIFLY_USERNAME', '"voiceon"'
buildConfigField 'String', 'NOTIFLY_PASSWORD', '"c6c585db0aaa4189be44d0467c7d66b6@A"'
buildConfigField 'String', 'KAKAO_APP_KEY', '"231cf78acfa8252fca38b9eedf87c5cb"'
buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"983594297130-5hrmkh6vpskeq6v34350kmilf74574h2.apps.googleusercontent.com"'
manifestPlaceholders = [
URISCHEME : "voiceon",
APPLINK_HOST : "voiceon.onelink.me",
FACEBOOK_APP_ID : "612448298237287",
FACEBOOK_CLIENT_TOKEN: "32af760f4a7b7cb7e3b1e7ffd0b0da70",
KAKAO_APP_KEY: "231cf78acfa8252fca38b9eedf87c5cb"
]
}
debug {
minifyEnabled false
minifyEnabled true
debuggable true
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', 'BOOTPAY_APP_HECTO_ID', '"667fca5d3bab7404f831c3e4"'
buildConfigField 'String', 'AGORA_APP_ID', '"b96574e191a9430fa54c605528aa3ef7"'
buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"ae18ade3afcf4086bd4397726eb0654c"'
buildConfigField 'String', 'NOTIFLY_PROJECT_ID', '"5f7ebe90d1ce5f0392164b8a53a662bc"'
buildConfigField 'String', 'NOTIFLY_USERNAME', '"voiceon"'
buildConfigField 'String', 'NOTIFLY_PASSWORD', '"c6c585db0aaa4189be44d0467c7d66b6@A"'
buildConfigField 'String', 'KAKAO_APP_KEY', '"20cf19413d63bfdfd30e8e6dff933d33"'
buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"758414412471-mosodbj2chno7l1j0iihldh6edmk0gk9.apps.googleusercontent.com"'
manifestPlaceholders = [
URISCHEME : "voiceon-test",
APPLINK_HOST : "voiceon-test.onelink.me",
FACEBOOK_APP_ID : "608674328645232",
FACEBOOK_CLIENT_TOKEN: "3775e6ea83236a685d264b6c5a1bbb4d",
KAKAO_APP_KEY: "20cf19413d63bfdfd30e8e6dff933d33"
]
}
}
compileOptions {
@@ -72,19 +95,24 @@ android {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
lint {
checkDependencies true
checkReleaseBuilds false
}
}
dependencies {
implementation "androidx.media:media:1.6.0"
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.media:media:1.7.0"
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'androidx.webkit:webkit:1.7.0'
implementation 'androidx.webkit:webkit:1.12.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.7'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7'
// Logger
implementation("com.orhanobut:logger:2.2.0") {
@@ -123,21 +151,24 @@ dependencies {
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'
implementation 'com.google.android.gms:play-services-oss-licenses:17.1.0'
// 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'
implementation 'androidx.credentials:credentials:1.3.0'
implementation 'androidx.credentials:credentials-play-services-auth:1.3.0'
implementation 'com.google.android.libraries.identity.googleid:googleid:1.1.1'
// bootpay
implementation "io.github.bootpay:android:4.3.4"
implementation "io.github.bootpay:android:4.4.3"
// agora
implementation "io.agora.rtc:voice-sdk:4.1.0-1"
implementation "io.agora.rtc:voice-sdk:4.2.6"
implementation 'io.agora.rtm:rtm-sdk:1.5.3'
// sound visualizer
@@ -148,4 +179,31 @@ dependencies {
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
implementation "com.michalsvec:single-row-calednar:1.0.0"
// google in-app-purchase
implementation "com.android.billingclient:billing-ktx:6.2.0"
// ROOM
kapt "androidx.room:room-compiler:2.5.0"
implementation "androidx.room:room-ktx:2.5.0"
implementation "androidx.room:room-runtime:2.5.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation "androidx.media3:media3-session:1.4.1"
implementation "androidx.media3:media3-exoplayer:1.4.1"
// Facebook
implementation "com.facebook.android:facebook-core:18.0.0"
// Appsflyer
implementation 'com.appsflyer:af-android-sdk:6.16.1'
// 노티플라이
implementation 'com.github.team-michael:notifly-android-sdk:1.12.0'
// Kakao
implementation "com.kakao.sdk:v2-common:2.21.0"
implementation "com.kakao.sdk:v2-auth:2.21.0"
implementation "com.kakao.sdk:v2-user:2.21.0"
}

View File

@@ -221,3 +221,19 @@
-keep class androidx.viewpager2.widget.**{*;}
-keep class kr.co.bootpay.core.** { *; }
-keep class retrofit2.** { *; }
-keep class com.google.gson.** { *; }
-keep class sun.misc.** { *; }
# @Keep 애노테이션이 붙은 클래스, 메서드, 필드를 보호
-keep @androidx.annotation.Keep class * { *; }
-keep class com.kakao.sdk.**.model.* { <fields>; }
-keep class * extends com.google.gson.TypeAdapter
# https://github.com/square/okhttp/pull/6792
-dontwarn org.bouncycastle.jsse.**
-dontwarn org.conscrypt.*
-dontwarn org.openjsse.**

View File

@@ -2,17 +2,25 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<queries>
<package android:name="com.facebook.katana" />
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<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"
@@ -28,11 +36,26 @@
<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" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
android:name=".app.SodaLiveApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/appsflyer_data_extraction_rules"
android:fullBackupContent="@xml/appsflyer_backup_rules"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -42,6 +65,28 @@
android:theme="@style/Theme.SodaLive"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".main.DeepLinkActivity"
android:exported="true">
<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:host="${APPLINK_HOST}"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="${URISCHEME}" />
</intent-filter>
</activity>
<activity
android:name=".splash.SplashActivity"
android:exported="true">
@@ -50,26 +95,21 @@
<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=".user.signup.SignUpActivity"
android:windowSoftInputMode="stateVisible" />
<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.charge.CanChargeActivity"
android:configChanges="orientation|screenSize|keyboardHidden" />
<activity android:name=".mypage.can.payment.CanPaymentActivity" />
<activity android:name=".mypage.can.payment.CanPaymentTempActivity" />
<activity android:name=".mypage.can.coupon.CanCouponActivity" />
<activity android:name=".live.room.create.LiveRoomCreateActivity" />
<activity android:name=".live.room.update.LiveRoomEditActivity" />
<activity android:name=".live.reservation.complete.LiveReservationCompleteActivity" />
@@ -79,8 +119,10 @@
<activity android:name=".explorer.profile.UserProfileActivity" />
<activity android:name=".explorer.profile.donation.UserProfileDonationAllViewActivity" />
<activity android:name=".explorer.profile.fantalk.UserProfileFantalkAllViewActivity" />
<activity android:name=".explorer.profile.CreatorNoticeWriteActivity" />
<activity android:name=".explorer.profile.follow.UserFollowerListActivity" />
<activity android:name=".explorer.profile.creator_community.all.CreatorCommunityAllActivity" />
<activity android:name=".explorer.profile.creator_community.write.CreatorCommunityWriteActivity" />
<activity android:name=".explorer.profile.creator_community.modify.CreatorCommunityModifyActivity" />
<activity android:name=".message.text.TextMessageWriteActivity" />
<activity android:name=".message.text.TextMessageDetailActivity" />
<activity android:name=".message.SelectMessageRecipientActivity" />
@@ -91,6 +133,7 @@
<activity android:name=".settings.event.EventActivity" />
<activity android:name=".settings.event.EventDetailActivity" />
<activity android:name=".settings.notification.NotificationSettingsActivity" />
<activity android:name=".settings.ContentSettingsActivity" />
<activity android:name=".live.reservation_status.LiveReservationStatusActivity" />
<activity android:name=".live.reservation_status.LiveReservationCancelActivity" />
<activity android:name=".audio_content.AudioContentActivity" />
@@ -102,10 +145,51 @@
<activity android:name=".live.now.all.LiveNowAllActivity" />
<activity android:name=".live.reservation.all.LiveReservationAllActivity" />
<activity android:name=".mypage.service_center.ServiceCenterActivity" />
<activity android:name=".message.MessageActivity" />
<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=".audio_content.all.by_theme.AudioContentAllByThemeActivity" />
<activity android:name=".live.roulette.config.RouletteConfigActivity" />
<activity android:name=".live.room.menu.MenuConfigActivity" />
<activity android:name=".audio_content.series.SeriesListAllActivity" />
<activity android:name=".audio_content.series.detail.SeriesDetailActivity" />
<activity android:name=".audio_content.series.content.SeriesContentAllActivity" />
<activity android:name=".audio_content.playlist.detail.AudioContentPlaylistDetailActivity" />
<activity android:name=".audio_content.playlist.create.AudioContentPlaylistCreateActivity" />
<activity android:name=".audio_content.playlist.modify.AudioContentPlaylistModifyActivity" />
<activity android:name=".audio_content.box.AudioContentBoxActivity" />
<activity android:name=".audition.detail.AuditionDetailActivity" />
<activity android:name=".audition.role.AuditionRoleDetailActivity" />
<activity android:name=".audio_content.main.v2.AudioContentMainActivity" />
<activity android:name=".audio_content.main.v2.alarm.all.AlarmContentAllActivity" />
<activity android:name=".audio_content.main.v2.asmr.AsmrNewContentAllActivity" />
<activity android:name=".audio_content.main.v2.replay.ReplayNewContentAllActivity" />
<activity android:name=".audio_content.main.v2.free.introduce_creator.IntroduceCreatorActivity" />
<activity android:name=".audio_content.main.v2.series.origianl_audio_drama.OriginalAudioDramaContentAllActivity" />
<activity android:name=".audio_content.main.v2.series.completed.CompletedSeriesActivity" />
<activity android:name=".search.SearchActivity" />
<activity android:name=".mypage.alarm.AlarmListActivity" />
<activity android:name=".mypage.alarm.AddAlarmActivity" />
<activity android:name=".mypage.alarm.select_audio_content.AlarmSelectAudioContentActivity" />
<activity android:name=".mypage.block.BlockMemberActivity" />
<activity
android:name=".mypage.alarm.AlarmActivity"
android:exported="true"
android:showWhenLocked="true"
android:turnScreenOn="true">
<intent-filter>
<action android:name="com.example.alarmapp.ALARM_ACTION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
@@ -114,11 +198,38 @@
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
android:theme="@style/Theme.AppCompat.DayNight" />
<activity
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Redirect URI: "kakao${NATIVE_APP_KEY}://oauth" -->
<data android:host="oauth"
android:scheme="kakao${KAKAO_APP_KEY}" />
</intent-filter>
</activity>
<service
android:name=".common.SodaLiveService"
android:foregroundServiceType="microphone|mediaPlayback"
android:stopWithTask="false" />
<service android:name=".audio_content.AudioContentPlayService" />
<service
android:name=".audio_content.AudioContentPlayService"
android:foregroundServiceType="mediaPlayback"
android:stopWithTask="false" />
<service
android:name=".audio_content.player.AudioContentPlayerService"
android:exported="false"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>
<!-- [START firebase_service] -->
<service
@@ -130,10 +241,43 @@
</service>
<!-- [END firebase_service] -->
<!-- 부팅 시 알람 재설정을 위한 리시버 -->
<receiver
android:name=".mypage.alarm.receiver.AlarmBootReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".mypage.alarm.receiver.AlarmReceiver"
android:enabled="true"
android:exported="false" />
<!-- [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] -->
<!-- [START facebook] -->
<meta-data
android:name="com.facebook.sdk.ApplicationId"
android:value="${FACEBOOK_APP_ID}" />
<meta-data
android:name="com.facebook.sdk.ClientToken"
android:value="${FACEBOOK_CLIENT_TOKEN}" />
<meta-data
android:name="com.facebook.sdk.AdvertiserIDCollectionEnabled"
android:value="true" />
<activity
android:name="com.facebook.FacebookActivity"
android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" />
<!-- [END facebook] -->
</application>
</manifest>

View File

@@ -172,13 +172,14 @@ class Agora(
fun sendRawMessageToPeer(
receiverUid: String,
requestType: LiveRoomRequestType,
requestType: LiveRoomRequestType? = null,
rawMessage: ByteArray? = null,
onSuccess: () -> Unit
) {
val option = SendMessageOptions()
val message = rtmClient!!.createMessage()
message.rawMessage = requestType.toString().toByteArray()
message.rawMessage = rawMessage ?: requestType.toString().toByteArray()
rtmClient!!.sendMessageToPeer(
receiverUid,

View File

@@ -5,15 +5,26 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.appsflyer.AppsFlyerLib
import com.appsflyer.deeplink.DeepLinkResult
import com.facebook.FacebookSdk
import com.kakao.sdk.common.KakaoSdk
import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.di.AppDI
import kr.co.vividnext.sodalive.tracking.FirebaseTracking
import tech.notifly.Notifly
class SodaLiveApp : Application() {
class SodaLiveApp : Application(), DefaultLifecycleObserver {
override fun onCreate() {
super.onCreate()
super<Application>.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
Logger.addLogAdapter(object : AndroidLogAdapter() {
override fun isLoggable(priority: Int, tag: String?): Boolean {
@@ -26,6 +37,16 @@ class SodaLiveApp : Application() {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
SharedPreferenceManager.init(applicationContext)
ImageLoaderProvider.init(applicationContext)
FacebookSdk.fullyInitialize()
KakaoSdk.init(applicationContext, BuildConfig.KAKAO_APP_KEY)
setupAppsFlyer()
setupNotifly()
}
private fun isDebuggable(): Boolean {
@@ -41,10 +62,81 @@ class SodaLiveApp : Application() {
packageManager.getApplicationInfo(packageName, 0)
}
debuggable = 0 != appInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
} catch (e: PackageManager.NameNotFoundException) {
/* debuggable variable will remain false */
} catch (_: PackageManager.NameNotFoundException) {
}
return debuggable
}
private fun setupAppsFlyer() {
// Appsflyer SDK 초기화
AppsFlyerLib.getInstance().init("tWF2wbJ5nSkya5Ru9mGcPU", null, this)
AppsFlyerLib.getInstance().start(this)
// 딥링크 및 디퍼드 딥링크 처리
AppsFlyerLib.getInstance().subscribeForDeepLink { deepLinkResult ->
when (deepLinkResult.status) {
DeepLinkResult.Status.FOUND -> {
SharedPreferenceManager.alreadyTrackingAppLaunch = false
val deepLink = deepLinkResult.deepLink
SharedPreferenceManager.marketingLinkValue = deepLink?.getStringValue(
"deep_link_value"
) ?: ""
val marketingPid = deepLink?.getStringValue(
"deep_link_sub1"
)
if (marketingPid != null) {
SharedPreferenceManager.marketingPid = marketingPid
}
SharedPreferenceManager.marketingUtmSource = deepLink?.getStringValue(
"deep_link_sub2"
) ?: ""
SharedPreferenceManager.marketingUtmMedium = deepLink?.getStringValue(
"deep_link_sub3"
) ?: ""
SharedPreferenceManager.marketingUtmCampaign = deepLink?.getStringValue(
"deep_link_sub4"
) ?: ""
SharedPreferenceManager.marketingLinkValueId = deepLink?.getStringValue(
"deep_link_sub5"
)?.toLongOrNull() ?: 0L
logUtmInFirebase()
}
DeepLinkResult.Status.NOT_FOUND -> Logger.d("딥링크를 찾을 수 없습니다.")
DeepLinkResult.Status.ERROR -> Logger.d("딥링크 처리 중 오류 발생: ${deepLinkResult.error}")
}
}
}
private fun logUtmInFirebase() {
FirebaseTracking.logUtm()
}
private fun setupNotifly() {
Notifly.initialize(
applicationContext,
BuildConfig.NOTIFLY_PROJECT_ID,
BuildConfig.NOTIFLY_USERNAME,
BuildConfig.NOTIFLY_PASSWORD,
)
}
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
isAppInForeground = true
}
override fun onStop(owner: LifecycleOwner) {
isAppInForeground = false
}
companion object {
var isAppInForeground = false
}
}

View File

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

View File

@@ -14,6 +14,8 @@ 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.category.AudioContentCategoryAdapter
import kr.co.vividnext.sodalive.audio_content.category.GetCategoryListResponse
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
@@ -32,6 +34,7 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
private lateinit var loadingDialog: LoadingDialog
private lateinit var audioContentAdapter: AudioContentAdapter
private lateinit var categoryAdapter: AudioContentCategoryAdapter
private var userId: Long = 0
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
@@ -43,6 +46,7 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
) {
if (it.resultCode == Activity.RESULT_OK) {
viewModel.page = 1
viewModel.isLast = false
viewModel.getAudioContentList(userId = userId) { finish() }
}
}
@@ -54,6 +58,7 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
}
bindData()
viewModel.getCategoryList(userId = userId)
viewModel.getAudioContentList(userId = userId) { finish() }
}
@@ -70,6 +75,67 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
activityResultLauncher.launch(intent)
}
categoryAdapter = AudioContentCategoryAdapter {
viewModel.selectCategory(it, userId = userId)
}
binding.rvCategory.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvCategory.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()
}
categoryAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvCategory.adapter = categoryAdapter
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.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
@@ -188,6 +254,17 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
)
viewModel.getAudioContentList(userId = userId) { finish() }
}
viewModel.categoryListLiveData.observe(this) {
if (it.isNotEmpty()) {
binding.rvCategory.visibility = View.VISIBLE
val items = it as MutableList<GetCategoryListResponse>
items.add(0, GetCategoryListResponse(0, "전체"))
categoryAdapter.addItems(items = items)
} else {
binding.rvCategory.visibility = View.GONE
}
}
}
private fun deselectSort() {

View File

@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.audio_content
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
@@ -21,6 +22,12 @@ class AudioContentAdapter(
private val binding: ItemAudioContentBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentListItem) {
binding.ivPin.visibility = if (item.isPin) {
View.VISIBLE
} else {
View.GONE
}
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
@@ -33,17 +40,37 @@ class AudioContentAdapter(
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)
binding.tvPrice.visibility = View.GONE
binding.tvOwned.visibility = View.GONE
binding.tvRented.visibility = View.GONE
binding.tvSoldOut.visibility = View.GONE
binding.tvScheduledToOpen.visibility = if (item.isScheduledToOpen) {
View.VISIBLE
} else {
binding.tvPrice.text = item.price.moneyFormat()
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_can,
0,
0,
0
)
View.GONE
}
if (item.isOwned) {
binding.tvOwned.visibility = View.VISIBLE
} else if (item.isRented) {
binding.tvRented.visibility = View.VISIBLE
} else if (item.isSoldOut) {
binding.tvSoldOut.visibility = View.VISIBLE
} else {
binding.tvPrice.visibility = View.VISIBLE
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) }

View File

@@ -1,20 +1,36 @@
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.all.by_theme.GetContentByThemeResponse
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.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentCurationResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRanking
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.main.v2.alarm.GetContentMainTabAlarmResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.asmr.GetContentMainTabAsmrResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.content.GetContentMainTabContentResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.free.GetContentMainTabLiveFreeResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.home.GetContentMainTabHomeResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.replay.GetContentMainTabLiveReplayResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.series.GetContentMainTabSeriesResponse
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.player.GenerateUrlResponse
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
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 kr.co.vividnext.sodalive.settings.ContentType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
@@ -32,6 +48,8 @@ interface AudioContentApi {
@GET("/audio-content")
fun getAudioContentList(
@Query("creator-id") id: Long,
@Query("category-id") categoryId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort-type") sort: AudioContentViewModel.Sort,
@@ -43,6 +61,17 @@ interface AudioContentApi {
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentThemeResponse>>>
@GET("/audio-content/theme/{id}/content")
fun getAudioContentByTheme(
@Path("id") id: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort-type") sort: AudioContentViewModel.Sort,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentByThemeResponse>>
@POST("/audio-content")
@Multipart
fun uploadAudioContent(
@@ -122,17 +151,25 @@ interface AudioContentApi {
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/main")
fun getMain(
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentMainResponse>>
@GET("/audio-content/main/new")
fun getNewContentOfTheme(
@Query("theme") theme: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@GET("/audio-content/main/new/all")
fun getNewContentAllOfTheme(
@Query("isFree") isFree: Boolean,
@Query("theme") theme: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetNewContentAllResponse>>
@POST("/audio-content/donation")
fun donation(
@Body request: AudioContentDonationRequest,
@@ -144,4 +181,255 @@ interface AudioContentApi {
@Body request: ModifyCommentRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/curation/{id}")
fun getAudioContentListByCurationId(
@Path("id") id: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@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(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@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>>
@GET("/audio-content/main/curation-list")
fun getCurationList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentCurationResponse>>>
@GET("/audio-content/main/banner-list")
fun getMainBannerList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentBannerResponse>>>
@GET("/audio-content/main/order-list")
fun getMainOrderList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@POST("/audio-content/pin-to-the-top/{id}")
fun pinContent(
@Path("id") audioContentId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@PUT("/audio-content/unpin-at-the-top/{id}")
fun unpinContent(
@Path("id") audioContentId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/{id}/generate-url")
fun generateUrl(
@Path("id") contentId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GenerateUrlResponse>>
@GET("/v2/audio-content/main/home")
fun getContentMainHome(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabHomeResponse>>
@GET("/v2/audio-content/main/home/popular-content-by-creator")
fun getPopularContentByCreator(
@Query("creatorId") creatorId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/home/content/ranking")
fun getContentMainHomeContentRanking(
@Query("sort-type") sortType: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/series")
fun getContentMainSeries(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabSeriesResponse>>
@GET("/v2/audio-content/main/series/original")
fun getOriginalAudioDramaList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetSeriesListResponse>>
@GET("/v2/audio-content/main/series/recommend-by-genre")
fun getRecommendSeriesListByGenre(
@Query("genreId") genreId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
@GET("/v2/audio-content/main/series/recommend-series-by-creator")
fun getRecommendSeriesByCreator(
@Query("creatorId") creatorId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
@GET("/v2/audio-content/main/series/completed-rank")
fun getCompletedSeries(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetSeriesListResponse>>
@GET("/v2/audio-content/main/content")
fun getContentMainContent(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabContentResponse>>
@GET("/v2/audio-content/main/content/new-content-by-theme")
fun getContentMainNewContentOfTheme(
@Query("theme") theme: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@GET("/v2/audio-content/main/content/ranking")
fun getDailyContentRanking(
@Query("sort-type") sortType: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/content/popular-content-by-creator")
fun getContentMainContentPopularContentByCreator(
@Query("creatorId") creatorId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/alarm")
fun getContentMainAlarm(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabAlarmResponse>>
@GET("/v2/audio-content/main/alarm/all")
fun getContentMainAlarmAll(
@Query("theme") theme: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetNewContentAllResponse>>
@GET("/v2/audio-content/main/asmr")
fun getContentMainAsmr(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabAsmrResponse>>
@GET("/v2/audio-content/main/asmr/popular-content-by-creator")
fun getPopularAsmrContentByCreator(
@Query("creatorId") creatorId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/replay")
fun getContentMainReplay(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabLiveReplayResponse>>
@GET("/v2/audio-content/main/replay/popular-content-by-creator")
fun getPopularReplayContentByCreator(
@Query("creatorId") creatorId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/free")
fun getContentMainFree(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabLiveFreeResponse>>
@GET("/v2/audio-content/main/free/introduce-creator")
fun getIntroduceCreatorList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@GET("/v2/audio-content/main/free/new-content-by-theme")
fun getNewFreeContentOfTheme(
@Query("theme") theme: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@GET("/v2/audio-content/main/free/popular-content-by-creator")
fun getPopularFreeContentByCreator(
@Query("creatorId") creatorId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/content/recommend-content-by-tag")
fun getRecommendedContentByTag(
@Query("tag") tag: String,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
}

View File

@@ -6,6 +6,7 @@ import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.media.AudioAttributes
@@ -51,6 +52,7 @@ class AudioContentPlayService :
putExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, mediaPlayer.currentPosition)
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
intent.setPackage(packageName)
sendBroadcast(intent)
handler.postDelayed(this, 1000)
}
@@ -64,6 +66,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI,
true
@@ -101,6 +104,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
true
@@ -136,6 +140,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
false
@@ -198,6 +203,14 @@ class AudioContentPlayService :
}
}
MusicAction.SEEK_BACKWARD.name -> {
seekBackward10Seconds()
}
MusicAction.SEEK_FORWARD.name -> {
seekForward10Seconds()
}
else -> {
val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
if (contentId != null && this.contentId == contentId) {
@@ -205,6 +218,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PAUSE
@@ -220,6 +234,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PLAY
@@ -254,6 +269,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_LOADING,
true
@@ -306,6 +322,23 @@ class AudioContentPlayService :
}
}
private fun seekForward10Seconds() {
if (this::mediaPlayer.isInitialized && mediaPlayer.isPlaying) {
val currentPosition = mediaPlayer.currentPosition
val duration = mediaPlayer.duration
val newPosition = (currentPosition + 10_000).coerceAtMost(duration)
mediaPlayer.seekTo(newPosition)
}
}
private fun seekBackward10Seconds() {
if (this::mediaPlayer.isInitialized && mediaPlayer.isPlaying) {
val currentPosition = mediaPlayer.currentPosition
val newPosition = (currentPosition - 10_000).coerceAtLeast(0)
mediaPlayer.seekTo(newPosition)
}
}
private fun toggleIsPlaying(isPlaying: Boolean? = null) {
this.isPlaying = isPlaying ?: !this.isPlaying
if (this.isPlaying) {
@@ -317,6 +350,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI,
true
@@ -341,6 +375,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
this@AudioContentPlayService.isPlaying
@@ -367,6 +402,7 @@ class AudioContentPlayService :
mediaPlayer.setOnCompletionListener(this)
mediaPlayer.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
@@ -378,6 +414,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PLAY
@@ -403,6 +440,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
false
@@ -468,7 +506,7 @@ class AudioContentPlayService :
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
val notificationBuilder = NotificationCompat
.Builder(this@AudioContentPlayService, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(resource)
.setContentTitle(title ?: "오디오 콘텐츠")
.setContentText(nickname ?: "")
@@ -498,7 +536,16 @@ class AudioContentPlayService :
.setShowActionsInCompactView(0, 1)
)
startForeground(1, notificationBuilder.build())
val notification = notificationBuilder.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
1,
notification,
FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
)
} else {
startForeground(1, notification)
}
}
override fun onLoadCleared(placeholder: Drawable?) {
@@ -556,6 +603,6 @@ class AudioContentPlayService :
}
enum class MusicAction {
PLAY, PAUSE, STOP, PROGRESS, INIT, CONDITIONAL_STOP
PLAY, PAUSE, STOP, PROGRESS, INIT, CONDITIONAL_STOP, SEEK_FORWARD, SEEK_BACKWARD
}
}

View File

@@ -1,27 +1,49 @@
package kr.co.vividnext.sodalive.audio_content
import kr.co.vividnext.sodalive.audio_content.category.CategoryApi
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 kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.ExplorerApi
import kr.co.vividnext.sodalive.settings.ContentType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import java.util.TimeZone
class AudioContentRepository(
private val api: AudioContentApi,
private val userApi: UserApi
private val categoryApi: CategoryApi,
private val explorerApi: ExplorerApi
) {
fun getAudioContentListByCurationId(
curationId: Long,
page: Int,
size: Int,
sort: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
token: String
) = api.getAudioContentListByCurationId(
id = curationId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
sort = sort,
authHeader = token
)
fun getAudioContentList(
id: Long,
categoryId: Long,
page: Int,
size: Int,
sort: AudioContentViewModel.Sort,
token: String
) = api.getAudioContentList(
id = id,
categoryId = categoryId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
page = page - 1,
size = size,
sort = sort,
@@ -66,22 +88,6 @@ class AudioContentRepository(
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,
@@ -115,10 +121,32 @@ class AudioContentRepository(
token: String
) = api.likeContent(request, authHeader = token)
fun getMain(token: String) = api.getMain(authHeader = token)
fun getNewContentOfTheme(theme: String, token: String) = api.getNewContentOfTheme(
theme = theme,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getNewContentAllOfTheme(
isFree: Boolean,
theme: String,
page: Int,
size: Int,
token: String
) = api.getNewContentAllOfTheme(
isFree = isFree,
theme = theme,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
authHeader = token
)
fun getNewContentThemeList(token: String) = api.getNewContentThemeList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
@@ -135,4 +163,61 @@ class AudioContentRepository(
),
authHeader = token
)
fun getContentRankingSortType(token: String) = api.getContentRankingSortType(authHeader = token)
fun getContentRanking(
page: Int,
size: Int,
sortType: String = "매출",
token: String
) = api.getContentRanking(
page = page - 1,
size = size,
sortType = sortType,
authHeader = token
)
fun getCurationList(page: Int, size: Int, token: String) = api.getCurationList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
authHeader = token
)
fun getMainBannerList(token: String) = api.getMainBannerList(authHeader = token)
fun getMainOrderList(token: String) = api.getMainOrderList(authHeader = token)
fun pinContent(
audioContentId: Long,
token: String
) = api.pinContent(audioContentId, authHeader = token)
fun unpinContent(
audioContentId: Long,
token: String
) = api.unpinContent(audioContentId, authHeader = token)
fun getCategoryList(
creatorId: Long,
token: String
) = categoryApi.getCategoryList(creatorId, authHeader = token)
fun getAudioContentByTheme(
themeId: Long,
page: Int,
size: Int,
sort: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
token: String
) = api.getAudioContentByTheme(
id = themeId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
sort = sort,
authHeader = token
)
fun getCreatorRank(token: String) = explorerApi.getCreatorRank(authHeader = token)
}

View File

@@ -6,6 +6,7 @@ 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.audio_content.category.GetCategoryListResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListResponse
@@ -24,6 +25,10 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
val audioContentListLiveData: LiveData<GetAudioContentListResponse>
get() = _audioContentListLiveData
private var _categoryListLiveData = MutableLiveData<List<GetCategoryListResponse>>()
val categoryListLiveData: LiveData<List<GetCategoryListResponse>>
get() = _categoryListLiveData
private val _sort = MutableLiveData(Sort.NEWEST)
val sort: LiveData<Sort>
get() = _sort
@@ -39,9 +44,10 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
PRICE_LOW
}
private var isLast = false
var isLast = false
var page = 1
private val size = 10
private var selectedCategoryId = 0L
fun getAudioContentList(userId: Long, onFailure: (() -> Unit)? = null) {
if (!_isLoading.value!! && !isLast) {
@@ -49,6 +55,7 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
compositeDisposable.add(
repository.getAudioContentList(
id = userId,
categoryId = selectedCategoryId,
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}",
@@ -59,12 +66,12 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
page += 1
_audioContentListLiveData.postValue(it.data!!)
} else {
if (it.data.items.isEmpty()) {
isLast = true
}
page += 1
_audioContentListLiveData.postValue(it.data!!)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
@@ -99,4 +106,44 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
isLast = false
_sort.postValue(sort)
}
fun selectCategory(categoryId: Long, userId: Long) {
isLast = false
page = 1
selectedCategoryId = categoryId
getAudioContentList(userId = userId)
}
fun getCategoryList(userId: Long) {
compositeDisposable.add(
repository.getCategoryList(
creatorId = userId,
token = "Bearer ${SharedPreferenceManager.token}",
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_categoryListLiveData.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

@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.audio_content
import androidx.annotation.Keep
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import java.text.SimpleDateFormat
@@ -7,6 +8,7 @@ import java.util.Date
import java.util.Locale
@Entity
@Keep
data class PlaybackTracking(
@Id
var id: Long = 0,

View File

@@ -0,0 +1,14 @@
package kr.co.vividnext.sodalive.audio_content
import com.google.gson.annotations.SerializedName
enum class PurchaseOption {
@SerializedName("BOTH")
BOTH,
@SerializedName("BUY_ONLY")
BUY_ONLY,
@SerializedName("RENT_ONLY")
RENT_ONLY,
}

View File

@@ -0,0 +1,187 @@
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.annotation.OptIn
import androidx.media3.common.util.UnstableApi
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.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
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
@OptIn(UnstableApi::class)
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
private var isFree: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
isFree = intent.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, false)
super.onCreate(savedInstanceState)
bindData()
viewModel.getThemeList()
viewModel.getNewContentList(isFree = isFree)
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = if (isFree) {
"새로운 무료 콘텐츠"
} else {
"새로운 단편"
}
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.tvNotice.text = if (isFree) {
"※ 최근 2주간 등록된 새로운 콘텐츠 입니다."
} else {
"※ 최근 2주간 등록된 새로운 단편 입니다."
}
setupNewContentTheme()
setupNewContent()
}
@SuppressLint("NotifyDataSetChanged")
private fun setupNewContentTheme() {
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
newContentAdapter.clear()
newContentAdapter.notifyDataSetChanged()
viewModel.selectTheme(it, isFree = isFree)
}
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() {
val spanCount = 3
val spacing = 40
newContentAdapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
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, spanCount)
binding.rvContent.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
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(isFree)
}
}
})
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) {
if (newContentAdapter.itemCount > 0 || it.isNotEmpty()) {
binding.rvContent.visibility = View.VISIBLE
binding.llNoItems.visibility = View.GONE
} else {
binding.rvContent.visibility = View.GONE
binding.llNoItems.visibility = View.VISIBLE
}
newContentAdapter.addItems(it)
}
viewModel.newContentTotalCountLiveData.observe(this) {
binding.tvTotalCount.text = "$it"
}
}
}

View File

@@ -0,0 +1,110 @@
package kr.co.vividnext.sodalive.audio_content.all
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
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.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.databinding.ItemAudioContentNewAllBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
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 context: Context,
private val binding: ItemAudioContentNewAllBinding,
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentMainItem) {
Glide
.with(context)
.load(item.coverImageUrl)
.apply(
RequestOptions().transform(
CenterCrop(),
RoundedCorners(8)
)
)
.into(binding.ivAudioContentCoverImage)
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
if (item.price > 0) {
binding.ivCan.visibility = View.VISIBLE
binding.tvCan.text = item.price.moneyFormat()
} else {
binding.ivCan.visibility = View.GONE
binding.tvCan.text = "무료"
}
binding.tvTime.text = item.duration
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(
parent.context,
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,129 @@
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(isFree: Boolean = false) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getNewContentAllOfTheme(
isFree = isFree,
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
} else {
isLast = true
}
_newContentListLiveData.postValue(it.data.items)
_newContentTotalCountLiveData.postValue(it.data.totalCount)
} 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, isFree: Boolean) {
isLast = false
page = 1
selectedTheme = theme
getNewContentList(isFree)
}
}

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.new_content.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,11 @@
package kr.co.vividnext.sodalive.audio_content.all
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
@Keep
data class GetNewContentAllResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentMainItem>
)

View File

@@ -0,0 +1,179 @@
package kr.co.vividnext.sodalive.audio_content.all.by_theme
import android.content.Intent
import android.os.Bundle
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
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.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentAllByThemeBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import org.koin.android.ext.android.inject
class AudioContentAllByThemeActivity : BaseActivity<ActivityAudioContentAllByThemeBinding>(
ActivityAudioContentAllByThemeBinding::inflate
) {
private val viewModel: AudioContentAllByThemeViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentNewAllAdapter
private var themeId: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
themeId = intent.getLongExtra(Constants.EXTRA_THEME_ID, 0)
if (themeId <= 0) {
Toast.makeText(
applicationContext,
"잘못된 요청입니다.\n다시 시도해 주세요.",
Toast.LENGTH_LONG
).show()
finish()
}
super.onCreate(savedInstanceState)
bindData()
viewModel.getContentList(themeId = themeId)
}
@OptIn(UnstableApi::class)
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.setOnClickListener { finish() }
val spanCount = 3
val spacing = 40
adapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
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.rvContentAll.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContentAll.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
binding.rvContentAll.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(themeId = themeId)
}
}
})
binding.rvContentAll.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)
}
}
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.rvContentAll.scrollToPosition(0)
}
binding.tvTotalCount.text = "${it.totalCount}"
binding.toolbar.tvBack.text = it.theme
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(themeId = themeId)
}
}
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.all.by_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.audio_content.AudioContentViewModel
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentAllByThemeViewModel(
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<GetContentByThemeResponse>()
val contentListLiveData: LiveData<GetContentByThemeResponse>
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 = 15
fun getContentList(themeId: Long) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentByTheme(
themeId = themeId,
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,12 @@
package kr.co.vividnext.sodalive.audio_content.all.by_theme
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
@Keep
data class GetContentByThemeResponse(
@SerializedName("theme") val theme: String,
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentMainItem>
)

View File

@@ -0,0 +1,82 @@
package kr.co.vividnext.sodalive.audio_content.box
import android.os.Handler
import android.os.Looper
import android.widget.LinearLayout
import com.google.android.material.tabs.TabLayout
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListFragment
import kr.co.vividnext.sodalive.audio_content.playlist.AudioContentPlaylistListFragment
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentBoxBinding
class AudioContentBoxActivity : BaseActivity<ActivityAudioContentBoxBinding>(
ActivityAudioContentBoxBinding::inflate
) {
private var startTabPosition = 0
override fun setupView() {
startTabPosition = intent.getIntExtra(Constants.EXTRA_START_TAB_POSITION, 0)
setupToolbar()
setupTabs()
binding.tabs.getTabAt(startTabPosition)?.select()
supportFragmentManager.beginTransaction()
.replace(
R.id.fl_container,
if (startTabPosition == 1) {
AudioContentPlaylistListFragment()
} else {
AudioContentOrderListFragment()
}
)
.commit()
}
private fun setupToolbar() {
binding.toolbar.tvBack.text = "내 보관함"
binding.toolbar.tvBack.setOnClickListener { finish() }
}
private fun setupTabs() {
val tabs = binding.tabs
tabs.addTab(tabs.newTab().setText("구매목록"))
tabs.addTab(tabs.newTab().setText("재생목록"))
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
tab.view.isSelected = true
when (tab.position) {
1 -> supportFragmentManager.beginTransaction()
.replace(R.id.fl_container, AudioContentPlaylistListFragment())
.commit()
else -> supportFragmentManager.beginTransaction()
.replace(R.id.fl_container, AudioContentOrderListFragment())
.commit()
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {
tab.view.isSelected = false
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
val tabStrip = tabs.getChildAt(0) as LinearLayout
for (i in 0 until tabStrip.childCount) {
val tab = tabStrip.getChildAt(i)
val params = tab.layoutParams as LinearLayout.LayoutParams
params.setMargins(12, 0, 12, 0)
params.height = LinearLayout.LayoutParams.WRAP_CONTENT
tab.layoutParams = params
tab.minimumHeight = 0
}
}
}

View File

@@ -0,0 +1,79 @@
package kr.co.vividnext.sodalive.audio_content.category
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.ItemContentCategoryBinding
class AudioContentCategoryAdapter(
private val onClick: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentCategoryAdapter.ViewHolder>() {
private val items = mutableListOf<GetCategoryListResponse>()
private var selectedCategory = ""
inner class ViewHolder(
private val context: Context,
private val binding: ItemContentCategoryBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(item: GetCategoryListResponse) {
if (
item.category == selectedCategory ||
(selectedCategory == "" && item.category == "전체")
) {
binding.tvCategory.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
)
binding.tvCategory.setTextColor(
ContextCompat.getColor(context, R.color.color_3bb9f1)
)
} else {
binding.tvCategory.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_777777
)
binding.tvCategory.setTextColor(
ContextCompat.getColor(context, R.color.color_777777)
)
}
binding.tvCategory.text = item.category
binding.root.setOnClickListener {
onClick(item.categoryId)
selectedCategory = item.category
notifyDataSetChanged()
}
}
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetCategoryListResponse>) {
this.selectedCategory = ""
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemContentCategoryBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
fun clear() {
this.items.clear()
}
}

View File

@@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.audio_content.category
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface CategoryApi {
@GET("/category")
fun getCategoryList(
@Query("creatorId") creatorId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetCategoryListResponse>>>
}

View File

@@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.audio_content.category
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class GetCategoryListResponse(
@SerializedName("categoryId") val categoryId: Long,
@SerializedName("category") val category: String
)

View File

@@ -19,7 +19,8 @@ class AudioContentCommentAdapter(
private val creatorId: Long,
private val modifyComment: (Long, String) -> Unit,
private val onClickDelete: (Long) -> Unit,
private val onItemClick: (GetAudioContentCommentListItem) -> Unit
private val onItemClick: (GetAudioContentCommentListItem) -> Unit,
private val onClickProfile: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentCommentAdapter.ViewHolder>() {
var items = mutableSetOf<GetAudioContentCommentListItem>()
@@ -30,12 +31,26 @@ class AudioContentCommentAdapter(
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentCommentListItem) {
binding.rlCommentModify.visibility = View.GONE
binding.tvComment.visibility = View.VISIBLE
binding.tvSecret.visibility = if (item.isSecret) {
View.VISIBLE
} else {
View.GONE
}
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
binding.ivCommentProfile.setOnClickListener {
if (SharedPreferenceManager.userId != item.writerId) {
onClickProfile(item.writerId)
}
}
val tvCommentLayoutParams = binding.tvComment.layoutParams as LinearLayout.LayoutParams
val can = item.donationCan
if (can > 0) {
@@ -44,27 +59,27 @@ class AudioContentCommentAdapter(
binding.tvDonationCan.text = can.moneyFormat()
binding.llDonationCan.setBackgroundResource(
when {
can >= 100000 -> {
can >= 10000 -> {
R.drawable.bg_round_corner_10_7_973a3a
}
can >= 50000 -> {
can >= 5000 -> {
R.drawable.bg_round_corner_10_7_d85e37
}
can >= 10000 -> {
can >= 1000 -> {
R.drawable.bg_round_corner_10_7_d38c38
}
can >= 5000 -> {
can >= 500 -> {
R.drawable.bg_round_corner_10_7_59548f
}
can >= 1000 -> {
can >= 100 -> {
R.drawable.bg_round_corner_10_7_4d6aa4
}
can >= 500 -> {
can >= 50 -> {
R.drawable.bg_round_corner_10_7_2d7390
}
@@ -99,12 +114,16 @@ class AudioContentCommentAdapter(
showOptionMenu(
context,
binding.ivMenu,
commentId = item.id,
writerId = item.writerId,
creatorId = creatorId,
onClickModify = {
binding.rlCommentModify.visibility = View.VISIBLE
binding.tvComment.visibility = View.GONE
},
onClickDelete = {
binding.rlCommentModify.visibility = View.GONE
binding.tvComment.visibility = View.VISIBLE
onClickDelete(item.id)
}
)
}
@@ -141,10 +160,10 @@ class AudioContentCommentAdapter(
private fun showOptionMenu(
context: Context,
v: View,
commentId: Long,
writerId: Long,
creatorId: Long,
onClickModify: () -> Unit
onClickModify: () -> Unit,
onClickDelete: () -> Unit
) {
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
@@ -162,7 +181,7 @@ class AudioContentCommentAdapter(
}
R.id.menu_review_delete -> {
onClickDelete(commentId)
onClickDelete()
}
}

View File

@@ -14,7 +14,8 @@ import kr.co.vividnext.sodalive.databinding.DialogAudioContentCommentBinding
class AudioContentCommentFragment(
private val creatorId: Long,
private val audioContentId: Long
private val audioContentId: Long,
private val isShowSecret: Boolean
) : BottomSheetDialogFragment() {
private lateinit var binding: DialogAudioContentCommentBinding
@@ -50,7 +51,8 @@ class AudioContentCommentFragment(
val commentListFragmentTag = "COMMENT_LIST_FRAGMENT"
val commentListFragment = AudioContentCommentListFragment.newInstance(
creatorId = creatorId,
audioContentId = audioContentId
audioContentId = audioContentId,
isShowSecret = isShowSecret
)
val fragmentTransaction = childFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.fl_container, commentListFragment, commentListFragmentTag)

View File

@@ -20,6 +20,7 @@ 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.dialog.MemberProfileDialog
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
@@ -35,6 +36,7 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
private var creatorId: Long = 0
private var audioContentId: Long = 0
private var isShowSecret: Boolean = false
override fun onCreateView(
inflater: LayoutInflater,
@@ -43,6 +45,7 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
): View? {
creatorId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID) ?: 0
audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0
isShowSecret = arguments?.getBoolean(Constants.EXTRA_IS_SHOW_SECRET) ?: false
return super.onCreateView(inflater, container, savedInstanceState)
}
@@ -73,8 +76,20 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
binding.ivCommentSend.setOnClickListener {
hideKeyboard()
val comment = binding.etComment.text.toString()
val isSecret = binding.tvSecret.isSelected
viewModel.registerComment(audioContentId, comment, isSecret)
binding.etComment.setText("")
viewModel.registerComment(audioContentId, comment)
binding.tvSecret.isSelected = false
}
if (isShowSecret) {
binding.tvSecret.visibility = View.VISIBLE
binding.tvSecret.setOnClickListener {
binding.tvSecret.isSelected = !binding.tvSecret.isSelected
}
} else {
binding.tvSecret.visibility = View.GONE
}
adapter = AudioContentCommentAdapter(
@@ -107,6 +122,14 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
},
onItemClick = {
(parentFragment as AudioContentCommentFragment).onClickComment(it)
},
onClickProfile = {
MemberProfileDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
memberId = it,
screenWidth = screenWidth
).show()
}
)
@@ -202,10 +225,15 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
}
companion object {
fun newInstance(creatorId: Long, audioContentId: Long): AudioContentCommentListFragment {
fun newInstance(
creatorId: Long,
audioContentId: Long,
isShowSecret: Boolean
): AudioContentCommentListFragment {
val args = Bundle()
args.putLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, creatorId)
args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
args.putBoolean(Constants.EXTRA_IS_SHOW_SECRET, isShowSecret)
val fragment = AudioContentCommentListFragment()
fragment.arguments = args

View File

@@ -84,7 +84,7 @@ class AudioContentCommentListViewModel(
}
}
fun registerComment(contentId: Long, comment: String) {
fun registerComment(contentId: Long, comment: String, isSecret: Boolean) {
if (!_isLoading.value!!) {
_isLoading.value = true
}
@@ -93,6 +93,7 @@ class AudioContentCommentListViewModel(
repository.registerComment(
contentId = contentId,
comment = comment,
isSecret = isSecret,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())

View File

@@ -124,27 +124,27 @@ class AudioContentCommentReplyHeaderViewHolder(
binding.tvDonationCan.text = can.moneyFormat()
binding.llDonationCan.setBackgroundResource(
when {
can >= 100000 -> {
can >= 10000 -> {
R.drawable.bg_round_corner_10_7_973a3a
}
can >= 50000 -> {
can >= 5000 -> {
R.drawable.bg_round_corner_10_7_d85e37
}
can >= 10000 -> {
can >= 1000 -> {
R.drawable.bg_round_corner_10_7_d38c38
}
can >= 5000 -> {
can >= 500 -> {
R.drawable.bg_round_corner_10_7_59548f
}
can >= 1000 -> {
can >= 100 -> {
R.drawable.bg_round_corner_10_7_4d6aa4
}
can >= 500 -> {
can >= 50 -> {
R.drawable.bg_round_corner_10_7_2d7390
}
@@ -179,6 +179,8 @@ class AudioContentCommentReplyItemViewHolder(
) : AudioContentCommentReplyViewHolder(binding) {
override fun bind(item: GetAudioContentCommentListItem) {
binding.rlCommentModify.visibility = View.GONE
binding.tvComment.visibility = View.VISIBLE
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)

View File

@@ -8,12 +8,14 @@ class AudioContentCommentRepository(private val api: AudioContentApi) {
contentId: Long,
comment: String,
parentId: Long? = null,
isSecret: Boolean = false,
token: String
) = api.registerComment(
request = RegisterAudioContentCommentRequest(
comment = comment,
contentId = contentId,
parentId = parentId
parentId = parentId,
isSecret = isSecret
),
authHeader = token
)

View File

@@ -1,21 +1,25 @@
package kr.co.vividnext.sodalive.audio_content.comment
import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Keep
data class GetAudioContentCommentListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentCommentListItem>
)
@Parcelize
@Keep
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("isSecret") val isSecret: Boolean,
@SerializedName("donationCan") val donationCan: Int,
@SerializedName("date") val date: String,
@SerializedName("replyCount") val replyCount: Int

View File

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

View File

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

View File

@@ -0,0 +1,178 @@
package kr.co.vividnext.sodalive.audio_content.curation
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
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.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentCurationBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import org.koin.android.ext.android.inject
@OptIn(UnstableApi::class)
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() }
val spanCount = 3
val spacing = 40
adapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
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, spanCount)
binding.rvCuration.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
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,11 @@
package kr.co.vividnext.sodalive.audio_content.curation
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
@Keep
data class GetCurationContentResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentMainItem>
)

View File

@@ -0,0 +1,62 @@
package kr.co.vividnext.sodalive.audio_content.detail
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.ItemLiveRoomDetailUserBinding
class AudioContentBuyerAdapter : RecyclerView.Adapter<AudioContentBuyerAdapter.ViewHolder>() {
private val items = mutableListOf<ContentBuyer>()
inner class ViewHolder(
private val binding: ItemLiveRoomDetailUserBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: ContentBuyer) {
binding.tvNickname.text = item.nickname
binding.ivProfile.load(item.profileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
return ViewHolder(
ItemLiveRoomDetailUserBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
holder.bind(items[position])
}
override fun getItemCount() = items.count()
@SuppressLint("NotifyDataSetChanged")
fun addItems(buyerList: List<ContentBuyer>) {
items.addAll(buyerList)
notifyDataSetChanged()
}
@SuppressLint("NotifyDataSetChanged")
fun clear() {
items.clear()
notifyDataSetChanged()
}
}

View File

@@ -1,6 +1,8 @@
package kr.co.vividnext.sodalive.audio_content.detail
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -8,41 +10,57 @@ import android.content.IntentFilter
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.RelativeLayout
import android.widget.SeekBar
import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
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.PurchaseOption
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.audio_content.player.AudioContentPlayerService
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.common.Utils
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentDetailBinding
import kr.co.vividnext.sodalive.explorer.profile.CreatorFollowNotifyFragment
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.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.mypage.can.payment.CanPaymentTempActivity
import kr.co.vividnext.sodalive.report.ReportType
import org.koin.android.ext.android.inject
import kotlin.math.ceil
@UnstableApi
class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBinding>(
ActivityAudioContentDetailBinding::inflate
) {
@@ -57,21 +75,24 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
private val audioContentReceiver = AudioContentReceiver()
private var creatorId: Long = 0
private val handler = Handler(Looper.getMainLooper())
private var refresh = false
set(value) {
field = value
setResult(RESULT_OK)
}
private var title = ""
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
private lateinit var contentBuyerAdapter: AudioContentBuyerAdapter
private lateinit var audioContent: GetAudioContentDetailResponse
private lateinit var orderType: OrderType
private lateinit var imm: InputMethodManager
@SuppressLint("SetTextI18n")
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
binding.scrollView.scrollTo(0, 0)
binding.sbProgress.progress = 0
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_play)
binding.ivPlayOrPause.setImageResource(0)
binding.tvTotalDuration.text = " / 00:00:00"
binding.tvCurrentDuration.text = "00:00:00"
binding.rlPreviewAlert.visibility = View.GONE
@@ -84,28 +105,44 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
super.onCreate(savedInstanceState)
imm = getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager
if (audioContentId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
activityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
contentOrder(audioContent, orderType)
}
}
bindData()
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onResume() {
super.onResume()
val intentFilter = IntentFilter(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
registerReceiver(audioContentReceiver, intentFilter)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(audioContentReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(audioContentReceiver, intentFilter)
}
if (refresh) {
contentBuyerAdapter.clear()
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
}
override fun onPause() {
super.onPause()
unregisterReceiver(audioContentReceiver)
super.onPause()
}
override fun setupView() {
@@ -114,10 +151,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.tvBack.setOnClickListener { finish() }
binding.ivClosePreviewAlert.setOnClickListener { viewModel.toggleShowPreviewAlert() }
binding.ivMenu.setOnClickListener {
showOptionMenu(
this,
binding.ivMenu,
)
showOptionMenu()
}
creatorOtherContentAdapter = OtherContentAdapter {
@@ -137,6 +171,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
)
binding.swipeRefreshLayout.setOnRefreshListener {
contentBuyerAdapter.clear()
viewModel.getAudioContentDetail(
audioContentId = audioContentId
) { finish() }
@@ -239,16 +274,22 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
})
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
val ivCoverLp = binding.ivCover.layoutParams as RelativeLayout.LayoutParams
ivCoverLp.width = (screenWidth - 13.3f.dpToPx()).toInt()
ivCoverLp.height = (screenWidth - 13.3f.dpToPx()).toInt()
binding.ivCover.layoutParams = ivCoverLp
val flSoldOutLp = binding.flSoldOut.layoutParams as RelativeLayout.LayoutParams
flSoldOutLp.width = (screenWidth - 13.3f.dpToPx()).toInt()
flSoldOutLp.height = (screenWidth - 13.3f.dpToPx()).toInt()
binding.flSoldOut.layoutParams = flSoldOutLp
binding.ivPlayLoop.setOnClickListener { viewModel.togglePlayLoop() }
binding.llDonation.setOnClickListener {
val dialog = LiveRoomDonationDialog(
this,
LayoutInflater.from(this)
) { can, message ->
) { can, message, _ ->
if (can <= 0) {
showToast("1캔 이상 후원하실 수 있습니다.")
} else if (message.isBlank()) {
@@ -258,8 +299,38 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
}
dialog.show(screenWidth)
dialog.show(screenWidth - 26.7f.dpToPx().toInt())
}
setupBuyerList()
}
private fun setupBuyerList() {
val recyclerView = binding.rvBuyer
contentBuyerAdapter = AudioContentBuyerAdapter()
recyclerView.layoutManager = LinearLayoutManager(
this,
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)
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
})
recyclerView.adapter = contentBuyerAdapter
}
private fun donation(can: Int, message: String) {
@@ -268,50 +339,47 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
}
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()
private fun showOptionMenu() {
val dialog = AudioContentDetailMenuBottomSheetDialog(
isPin = viewModel.audioContentLiveData.value!!.isPin,
isCreator = viewModel.audioContentLiveData.value!!
.creator.creatorId == SharedPreferenceManager.userId,
onClickPin = {
if (viewModel.audioContentLiveData.value!!.isPin) {
viewModel.unPinContent(audioContentId)
} else {
if (viewModel.audioContentLiveData.value!!.isAvailablePin) {
viewModel.pinContent(audioContentId)
} else {
SodaDialog(
this@AudioContentDetailActivity,
layoutInflater,
"고정 한도 도달",
"이 콘텐츠를 고정하시겠어요? " +
"채널에 콘텐츠를 최대 3개까지 고정할 수 있습니다." +
"이 콘텐츠를 고정하면 가장 오래된 콘텐츠가 대체됩니다.",
"확인",
{ viewModel.pinContent(audioContentId) },
"취소",
{}
).show(screenWidth)
}
}
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()
},
onClickModify = {
refresh = true
setResult(RESULT_OK)
startActivity(
Intent(applicationContext, AudioContentModifyActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
}
)
},
onClickDelete = { showDeleteDialog() },
onClickReport = { showReportDialog() }
)
dialog.show(supportFragmentManager, dialog.tag)
}
private fun showDeleteDialog() {
@@ -384,12 +452,9 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
setupInfoArea(it)
setupPurchaseButton(it)
setupCommentArea(it)
setupPreviousNextContentArea(it.previousContent, it.nextContent)
setupCreatorOtherContentListArea(it.creatorOtherContentList)
setupSameThemeOtherContentList(it.sameThemeOtherContentList)
isAlertPreview = it.creator.creatorId != SharedPreferenceManager.userId &&
!it.existOrdered &&
it.price > 0
}
viewModel.isContentPlayLoopLiveData.observe(this) {
@@ -435,8 +500,84 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
}
private fun setupPreviousNextContentArea(
previousContent: OtherContentResponse?,
nextContent: OtherContentResponse?
) {
binding.llPreviousNextContent.visibility = if (
previousContent != null ||
nextContent != null
) {
View.VISIBLE
} else {
View.GONE
}
if (previousContent != null) {
binding.llPreviousContent.visibility = View.VISIBLE
binding.viewPreviousNone.visibility = View.GONE
binding.ivPreviousCover.load(previousContent.coverUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
binding.tvPreviousTitle.text = previousContent.title
binding.llPreviousContent.setOnClickListener {
startActivity(
Intent(
applicationContext,
AudioContentDetailActivity::class.java
).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, previousContent.contentId)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
)
}
} else {
binding.viewPreviousNone.visibility = View.VISIBLE
binding.llPreviousContent.visibility = View.GONE
}
if (nextContent != null) {
binding.llNextContent.visibility = View.VISIBLE
binding.viewNextNone.visibility = View.GONE
binding.ivNextCover.load(nextContent.coverUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
binding.tvNextTitle.text = nextContent.title
binding.llNextContent.setOnClickListener {
startActivity(
Intent(
applicationContext,
AudioContentDetailActivity::class.java
).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, nextContent.contentId)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
)
}
} else {
binding.viewNextNone.visibility = View.VISIBLE
binding.llNextContent.visibility = View.GONE
}
}
private fun setupCommentArea(response: GetAudioContentDetailResponse) {
if (response.isCommentAvailable) {
if (
response.isCommentAvailable &&
response.contentUrl.isNotBlank() &&
response.releaseDate == null
) {
binding.llDonation.visibility = View.VISIBLE
binding.llComment.visibility = View.VISIBLE
binding.tvCommentCount.text = "${response.commentCount}"
@@ -450,8 +591,13 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.tvCommentText.text = response.commentList[0].comment
binding.tvCommentText.visibility = View.VISIBLE
binding.rlInputComment.visibility = View.GONE
binding.tvSecret.visibility = View.GONE
binding.llComment.setOnClickListener { showCommentBottomSheetDialog() }
binding.llComment.setOnClickListener {
showCommentBottomSheetDialog(
isShowSecret = response.existOrdered
)
}
} else {
binding.tvCommentText.visibility = View.GONE
binding.rlInputComment.visibility = View.VISIBLE
@@ -462,9 +608,22 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
binding.ivCommentSend.setOnClickListener {
hideKeyboard()
val comment = binding.etComment.text.toString()
val isSecret = binding.tvSecret.isSelected
viewModel.registerComment(audioContentId, comment, isSecret)
binding.etComment.setText("")
viewModel.registerComment(audioContentId, comment)
binding.tvSecret.isSelected = false
}
if (response.existOrdered) {
binding.tvSecret.visibility = View.VISIBLE
binding.tvSecret.setOnClickListener {
binding.tvSecret.isSelected = !binding.tvSecret.isSelected
}
} else {
binding.tvSecret.visibility = View.GONE
}
binding.llComment.setOnClickListener {}
@@ -475,10 +634,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
}
private fun showCommentBottomSheetDialog() {
private fun showCommentBottomSheetDialog(isShowSecret: Boolean) {
val dialog = AudioContentCommentFragment(
creatorId = creatorId,
audioContentId = audioContentId
audioContentId = audioContentId,
isShowSecret = isShowSecret
)
dialog.show(
supportFragmentManager,
@@ -487,20 +647,96 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
private fun setupPurchaseButton(response: GetAudioContentDetailResponse) {
if (
if (response.releaseDate != null) {
binding.llPurchase.visibility = View.VISIBLE
binding.llPurchasePrice.visibility = View.GONE
binding.tvPurchaseSoldOut.visibility = View.GONE
binding.tvReleaseDate.visibility = View.VISIBLE
binding.llPurchase.background = ContextCompat.getDrawable(
applicationContext,
R.drawable.bg_round_corner_5_3_525252
)
binding.tvReleaseDate.text = response.releaseDate
} else if (
response.price > 0 &&
!response.existOrdered &&
response.orderType == null &&
response.creator.creatorId != SharedPreferenceManager.userId
) {
binding.llPurchase.visibility = View.VISIBLE
binding.tvPrice.text = response.price.toString()
if (
response.totalContentCount != null && response.remainingContentCount != null &&
response.remainingContentCount <= 0
) {
binding.llPurchase.visibility = View.GONE
binding.tvPurchaseSoldOut.visibility = View.VISIBLE
} else {
binding.tvPurchaseSoldOut.visibility = View.GONE
binding.tvReleaseDate.visibility = View.GONE
binding.llPurchase.visibility = View.VISIBLE
binding.llPurchasePrice.visibility = View.VISIBLE
binding.llPurchase.background = ContextCompat.getDrawable(
applicationContext,
R.drawable.bg_round_corner_5_3_3bb9f1
)
binding.llPurchase.setOnClickListener {
showOrderDialog(audioContent = response)
binding.ivCan.visibility = if (SharedPreferenceManager.userId == 17958L) {
View.GONE
} else {
View.VISIBLE
}
binding.tvPrice.text = if (SharedPreferenceManager.userId == 17958L) {
(response.price * 110).moneyFormat()
} else {
response.price.moneyFormat()
}
binding.tvUnit.text = if (SharedPreferenceManager.userId == 17958L) {
"원으로"
} else {
"캔으로"
}
when (response.purchaseOption) {
PurchaseOption.BOTH -> {
binding.tvStrPurchaseOrRental.text = " 구매하기"
binding.llPurchase.setBackgroundResource(
R.drawable.bg_round_corner_5_3_3bb9f1
)
}
PurchaseOption.BUY_ONLY -> {
binding.tvStrPurchaseOrRental.text = " 소장하기"
binding.llPurchase.setBackgroundResource(
R.drawable.bg_round_corner_5_3_59548f
)
}
PurchaseOption.RENT_ONLY -> {
binding.tvStrPurchaseOrRental.text = " 대여하기"
binding.llPurchase.setBackgroundResource(
R.drawable.bg_round_corner_5_3_548f7d
)
}
}
val limitedEdition = response.totalContentCount != null &&
response.remainingContentCount != null
binding.llPurchase.setOnClickListener {
if (limitedEdition || response.purchaseOption == PurchaseOption.BUY_ONLY) {
showOrderConfirmDialog(audioContent = response, OrderType.KEEP)
} else if (response.purchaseOption == PurchaseOption.RENT_ONLY) {
showOrderConfirmDialog(audioContent = response, OrderType.RENTAL)
} else {
showOrderDialog(audioContent = response)
}
}
}
} else {
binding.llPurchase.visibility = View.GONE
binding.tvPurchaseSoldOut.visibility = View.GONE
}
}
@@ -514,29 +750,118 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
.apply(RequestOptions().override((screenWidth - 13.3f.dpToPx()).toInt()))
.into(binding.ivCover)
binding.ivPlayOrPause.setOnClickListener {
startService(
Intent(this, AudioContentPlayService::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL, response.coverImageUrl)
putExtra(Constants.EXTRA_AUDIO_CONTENT_URL, response.contentUrl)
putExtra(Constants.EXTRA_NICKNAME, response.creator.nickname)
putExtra(Constants.EXTRA_AUDIO_CONTENT_TITLE, response.title)
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, response.contentId)
putExtra(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, response.creator.creatorId)
putExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, response.price <= 0)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PREVIEW,
!response.existOrdered && response.price > 0
)
binding.flSoldOut.visibility = View.GONE
binding.tvSoldOutBig.visibility = View.GONE
binding.ivPlayOrPause.visibility = View.GONE
binding.ivSeekBackward10.visibility = View.GONE
binding.ivSeekForward10.visibility = View.GONE
binding.tvPreviewNo.visibility = View.GONE
binding.tvTotalDuration.text = " / ${response.duration}"
isAlertPreview = response.creator.creatorId != SharedPreferenceManager.userId &&
!response.existOrdered &&
response.price > 0
if (
response.creator.creatorId != SharedPreferenceManager.userId && !response.existOrdered &&
response.totalContentCount != null && response.remainingContentCount != null &&
response.remainingContentCount <= 0
) {
binding.flSoldOut.visibility = View.VISIBLE
binding.tvSoldOutBig.visibility = View.VISIBLE
} else if (
response.releaseDate == null &&
!isAlertPreview ||
(response.isActivePreview && response.contentUrl.isNotBlank())
) {
binding.ivPlayOrPause.visibility = View.VISIBLE
binding.ivPlayOrPause.setOnClickListener {
startService(
Intent(
applicationContext,
AudioContentPlayerService::class.java
).apply {
action = "STOP_SERVICE"
}
)
startService(
Intent(
applicationContext,
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.ivPlayOrPause.setImageResource(
if (!isAlertPreview) {
R.drawable.btn_audio_content_play
} else {
R.drawable.btn_audio_content_preview_play
}
)
if (!isAlertPreview) {
binding.ivSeekForward10.visibility = View.VISIBLE
binding.ivSeekBackward10.visibility = View.VISIBLE
binding.ivSeekForward10.setOnClickListener {
startService(
Intent(
applicationContext,
AudioContentPlayService::class.java
).apply {
action = AudioContentPlayService.MusicAction.SEEK_FORWARD.name
}
)
}
binding.ivSeekBackward10.setOnClickListener {
startService(
Intent(
applicationContext,
AudioContentPlayService::class.java
).apply {
action = AudioContentPlayService.MusicAction.SEEK_BACKWARD.name
}
)
}
}
} else if (response.releaseDate == null) {
binding.tvPreviewNo.visibility = View.VISIBLE
}
binding.tvTotalDuration.text = " / ${response.duration}"
binding.ivPoint.visibility = if (response.isAvailableUsePoint) {
View.VISIBLE
} else {
View.GONE
}
}
@SuppressLint("SetTextI18n")
private fun setupInfoArea(response: GetAudioContentDetailResponse) {
binding.tvScheduledToOpen.visibility = if (response.releaseDate != null) {
View.VISIBLE
} else {
View.GONE
}
binding.tvTheme.text = response.themeStr
binding.tv19.visibility = if (response.isAdult) {
View.VISIBLE
@@ -570,45 +895,85 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
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)
if (response.contentUrl.isNotBlank() && response.releaseDate == null) {
binding.svActionButtons.visibility = View.VISIBLE
binding.llLike.visibility = View.VISIBLE
binding.ivLike.setImageResource(
if (response.isLike) {
R.drawable.ic_audio_content_heart_pressed
} else {
binding.tvLike.text = if (likeCount - 1 < 0) {
"0"
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 {
"${likeCount - 1}"
binding.tvLike.text = if (likeCount - 1 < 0) {
"0"
} else {
"${likeCount - 1}"
}
binding.ivLike.setImageResource(R.drawable.ic_audio_content_heart_normal)
}
binding.ivLike.setImageResource(R.drawable.ic_audio_content_heart_normal)
}
}
binding.tvShare.visibility = View.VISIBLE
binding.tvShare.setOnClickListener {
viewModel.shareContent(audioContentId = audioContentId) {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, it)
val shareIntent = Intent.createChooser(intent, "오디오 콘텐츠 공유")
startActivity(shareIntent)
}
}
} else {
binding.svActionButtons.visibility = View.GONE
binding.llLike.visibility = View.GONE
binding.tvShare.visibility = View.GONE
}
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)
if (response.totalContentCount != null && response.remainingContentCount != null) {
binding.rlLimitedEdition.visibility = View.VISIBLE
val shareIntent = Intent.createChooser(intent, "오디오콘텐츠 공유")
startActivity(shareIntent)
if (response.existOrdered) {
binding.tvRemaining.visibility = View.GONE
binding.tvTotalCount.visibility = View.VISIBLE
binding.tvRemainingCount.visibility = View.VISIBLE
binding.tvRemainingCount.text = "${response.orderSequence}"
binding.tvTotalCount.text = " / ${response.totalContentCount}"
} else if (response.remainingContentCount <= 0) {
binding.tvRemainingCount.visibility = View.GONE
binding.tvTotalCount.visibility = View.GONE
binding.tvRemaining.visibility = View.GONE
binding.tvSoldOutSmall.visibility = View.VISIBLE
} else {
binding.tvRemainingCount.visibility = View.VISIBLE
binding.tvRemaining.visibility = View.VISIBLE
binding.tvTotalCount.visibility = View.GONE
binding.tvRemainingCount.text = "${response.remainingContentCount}"
}
if (response.buyerList.isNotEmpty()) {
binding.rvBuyer.visibility = View.VISIBLE
binding.tvBuyerTitle.visibility = View.VISIBLE
contentBuyerAdapter.addItems(response.buyerList)
}
} else {
binding.rlLimitedEdition.visibility = View.GONE
binding.rvBuyer.visibility = View.GONE
binding.tvBuyerTitle.visibility = View.GONE
}
}
@@ -654,21 +1019,50 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
if (creator.creatorId != SharedPreferenceManager.userId) {
binding.ivFollow.visibility = View.VISIBLE
if (creator.isFollowing) {
binding.ivFollow.setImageResource(R.drawable.btn_notification_selected)
if (creator.isFollow) {
binding.ivFollow.setImageResource(
if (creator.isNotify) {
R.drawable.btn_following_big
} else {
R.drawable.btn_following_no_alarm_big
}
)
binding.ivFollow.setOnClickListener {
viewModel.unRegisterNotification(
contentId = audioContentId,
creatorId = creator.creatorId
val notifyFragment = CreatorFollowNotifyFragment(
onClickNotifyAll = {
viewModel.follow(
contentId = audioContentId,
creatorId = creator.creatorId,
follow = true,
notify = true
)
},
onClickNotifyNone = {
viewModel.follow(
contentId = audioContentId,
creatorId = creator.creatorId,
follow = true,
notify = false
)
},
onClickUnFollow = {
viewModel.follow(
contentId = audioContentId,
creatorId = creator.creatorId,
follow = false,
notify = false
)
}
)
if (notifyFragment.isAdded) return@setOnClickListener
notifyFragment.show(supportFragmentManager, notifyFragment.tag)
}
} else {
binding.ivFollow.setImageResource(R.drawable.btn_notification)
binding.ivFollow.setImageResource(R.drawable.btn_follow_big)
binding.ivFollow.setOnClickListener {
viewModel.registerNotification(
contentId = audioContentId,
creatorId = creator.creatorId
)
viewModel.follow(contentId = audioContentId, creatorId = creator.creatorId)
}
}
} else {
@@ -703,7 +1097,14 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
nickname = audioContent.creator.nickname,
duration = audioContent.duration,
orderType = orderType,
price = audioContent.price,
price = if (
audioContent.purchaseOption == PurchaseOption.BOTH &&
orderType == OrderType.RENTAL
) {
ceil(audioContent.price * 0.7).toInt()
} else {
audioContent.price
},
confirmButtonClick = {
startService(
Intent(this, AudioContentPlayService::class.java).apply {
@@ -714,14 +1115,52 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.rlPreviewAlert.visibility = View.GONE
viewModel.order(
contentId = audioContent.contentId,
orderType = orderType
)
if (SharedPreferenceManager.userId == 17958L) {
this@AudioContentDetailActivity.audioContent = audioContent
this@AudioContentDetailActivity.orderType = orderType
activityResultLauncher.launch(
Intent(applicationContext, CanPaymentTempActivity::class.java).apply {
putExtra("title", audioContent.title)
putExtra(
"can",
if (orderType == OrderType.RENTAL) {
ceil(audioContent.price * 0.7).toInt()
} else {
audioContent.price
}
)
}
)
} else {
contentOrder(audioContent, orderType)
}
},
).show(screenWidth)
}
private fun contentOrder(
audioContent: GetAudioContentDetailResponse,
orderType: OrderType
) {
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)
}
}
private fun hideKeyboard() {
handler.postDelayed({
imm.hideSoftInputFromWindow(
window.decorView.applicationWindowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
}, 100)
}
inner class AudioContentReceiver : BroadcastReceiver() {
@SuppressLint("SetTextI18n")
override fun onReceive(context: Context?, intent: Intent?) {
@@ -757,7 +1196,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
if (isPlaying != null && isPlaying) {
R.drawable.btn_audio_content_pause
} else {
R.drawable.btn_audio_content_play
if (isAlertPreview) {
R.drawable.btn_audio_content_preview_play
} else {
R.drawable.btn_audio_content_play
}
}
)
}

View File

@@ -0,0 +1,66 @@
package kr.co.vividnext.sodalive.audio_content.detail
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.R
import kr.co.vividnext.sodalive.databinding.DialogAudioContentDetailMenuBinding
class AudioContentDetailMenuBottomSheetDialog(
private val isPin: Boolean,
private val isCreator: Boolean,
private val onClickPin: () -> Unit,
private val onClickModify: () -> Unit,
private val onClickDelete: () -> Unit,
private val onClickReport: () -> Unit
) : BottomSheetDialogFragment() {
private lateinit var dialog: DialogAudioContentDetailMenuBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
dialog = DialogAudioContentDetailMenuBinding.inflate(inflater, container, false)
return dialog.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (isCreator) {
dialog.tvReport.visibility = View.GONE
dialog.llMenuCreator.visibility = View.VISIBLE
if (isPin) {
dialog.ivPin.setImageResource(R.drawable.ic_pin_cancel)
dialog.tvPin.text = "내 채널에 고정 취소"
} else {
dialog.ivPin.setImageResource(R.drawable.ic_pin)
dialog.tvPin.text = "내 채널에 고정"
}
dialog.llPin.setOnClickListener {
dismiss()
onClickPin()
}
dialog.llModify.setOnClickListener {
dismiss()
onClickModify()
}
dialog.llDelete.setOnClickListener {
dismiss()
onClickDelete()
}
} else {
dialog.llMenuCreator.visibility = View.GONE
dialog.tvReport.visibility = View.VISIBLE
dialog.tvReport.setOnClickListener {
dismiss()
onClickReport()
}
}
}
}

View File

@@ -1,16 +1,7 @@
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
@@ -19,15 +10,18 @@ import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentReposit
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.common.Utils
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
import kr.co.vividnext.sodalive.user.UserRepository
class AudioContentDetailViewModel(
private val repository: AudioContentRepository,
private val userRepository: UserRepository,
private val authRepository: AuthRepository,
private val reportRepository: ReportRepository,
private val commentRepository: AudioContentCommentRepository
@@ -102,45 +96,14 @@ class AudioContentDetailViewModel(
)
}
fun registerNotification(contentId: Long, creatorId: Long) {
fun follow(contentId: Long, creatorId: Long, follow: Boolean = true, notify: Boolean = true) {
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}"
userRepository.creatorFollow(
creatorId = creatorId,
follow,
notify,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@@ -176,7 +139,7 @@ class AudioContentDetailViewModel(
_isShowPreviewAlert.value = !_isShowPreviewAlert.value!!
}
fun order(contentId: Long, orderType: OrderType) {
fun order(contentId: Long, orderType: OrderType, gotoShop: () -> Unit) {
isLoading.value = true
compositeDisposable.add(
repository.orderContent(
@@ -190,10 +153,19 @@ class AudioContentDetailViewModel(
{
if (it.success && it.data != null) {
getAudioContentDetail(audioContentId = contentId)
_toastLiveData.postValue("구매가 완료되었습니다.")
_toastLiveData.postValue(
if (orderType == OrderType.RENTAL) {
"대여가 완료되었습니다."
} else {
"구매가 완료되었습니다."
}
)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
if (it.message.contains("캔이 부족합니다")) {
gotoShop()
}
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
@@ -245,7 +217,7 @@ class AudioContentDetailViewModel(
)
}
fun registerComment(audioContentId: Long, comment: String) {
fun registerComment(audioContentId: Long, comment: String, isSecret: Boolean) {
if (!isLoading.value!!) {
isLoading.value = true
}
@@ -254,6 +226,7 @@ class AudioContentDetailViewModel(
commentRepository.registerComment(
contentId = audioContentId,
comment = comment,
isSecret = isSecret,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
@@ -324,38 +297,18 @@ class AudioContentDetailViewModel(
)
}
fun shareAudioContent(
fun shareContent(
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
}
val params = mapOf(
"af_dp" to "voiceon://",
"deep_link_value" to "content",
"deep_link_sub5" to "$audioContentId"
)
val shareUrl = Utils.createOneLinkUrl(params = params)
onSuccess(shareUrl)
}
fun deleteAudioContent(audioContentId: Long, onSuccess: () -> Unit) {
@@ -472,4 +425,74 @@ class AudioContentDetailViewModel(
)
)
}
fun pinContent(audioContentId: Long) {
isLoading.value = true
compositeDisposable.add(
repository.pinContent(
audioContentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
isLoading.value = false
if (it.success) {
_toastLiveData.postValue("고정되었습니다.")
getAudioContentDetail(audioContentId)
} 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 unPinContent(audioContentId: Long) {
isLoading.value = true
compositeDisposable.add(
repository.unpinContent(
audioContentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
isLoading.value = false
if (it.success) {
_toastLiveData.postValue("해제되었습니다.")
getAudioContentDetail(audioContentId)
} 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

@@ -1,9 +1,12 @@
package kr.co.vividnext.sodalive.audio_content.detail
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.PurchaseOption
import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListItem
import kr.co.vividnext.sodalive.audio_content.order.OrderType
@Keep
data class GetAudioContentDetailResponse(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@@ -14,9 +17,15 @@ data class GetAudioContentDetailResponse(
@SerializedName("tag") val tag: String,
@SerializedName("price") val price: Int,
@SerializedName("duration") val duration: String,
@SerializedName("releaseDate") val releaseDate: String?,
@SerializedName("totalContentCount") val totalContentCount: Int?,
@SerializedName("remainingContentCount") val remainingContentCount: Int?,
@SerializedName("orderSequence") val orderSequence: Int?,
@SerializedName("isActivePreview") val isActivePreview: Boolean,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isMosaic") val isMosaic: Boolean,
@SerializedName("existOrdered") val existOrdered: Boolean,
@SerializedName("purchaseOption") val purchaseOption: PurchaseOption,
@SerializedName("orderType") val orderType: OrderType?,
@SerializedName("remainingTime") val remainingTime: String?,
@SerializedName("creatorOtherContentList")
@@ -28,18 +37,34 @@ data class GetAudioContentDetailResponse(
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentList") val commentList: List<GetAudioContentCommentListItem>,
@SerializedName("commentCount") val commentCount: Int,
@SerializedName("creator") val creator: AudioContentCreator
@SerializedName("isPin") val isPin: Boolean,
@SerializedName("isAvailablePin") val isAvailablePin: Boolean,
@SerializedName("creator") val creator: AudioContentCreator,
@SerializedName("previousContent") val previousContent: OtherContentResponse?,
@SerializedName("nextContent") val nextContent: OtherContentResponse?,
@SerializedName("buyerList") val buyerList: List<ContentBuyer>,
@SerializedName("isAvailableUsePoint") val isAvailableUsePoint: Boolean
)
@Keep
data class OtherContentResponse(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverUrl") val coverUrl: String,
)
@Keep
data class AudioContentCreator(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImageUrl") val profileImageUrl: String,
@SerializedName("isFollowing") val isFollowing: Boolean
@SerializedName("isFollowing") val isFollowing: Boolean,
@SerializedName("isFollow") var isFollow: Boolean,
@SerializedName("isNotify") var isNotify: Boolean
)
@Keep
data class ContentBuyer(
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImageUrl") val profileImageUrl: String
)

View File

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

View File

@@ -1,7 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.donation
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class AudioContentDonationRequest(
@SerializedName("contentId") val contentId: Long,
@SerializedName("donationCan") val donationCan: Int,

View File

@@ -1,63 +1,96 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.app.Service
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
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.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.all.by_theme.AudioContentAllByThemeActivity
import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity
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.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerViewModel
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationAdapter
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationViewModel
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentViewModel
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainCreatorRankingViewModel
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingAdapter
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingViewModel
import kr.co.vividnext.sodalive.audio_content.main.recommend_series.AudioContentMainRecommendSeriesViewModel
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
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.ExplorerSectionAdapter
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.explorer.profile.series.UserProfileSeriesListAdapter
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.mypage.alarm.AlarmListActivity
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
@OptIn(UnstableApi::class)
class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
FragmentAudioContentMainBinding::inflate
) {
private val viewModel: AudioContentMainViewModel by inject()
private val creatorRankViewModel: AudioContentMainCreatorRankingViewModel by inject()
private lateinit var creatorRankAdaptor: ExplorerSectionAdapter
private lateinit var loadingDialog: LoadingDialog
private lateinit var imm: InputMethodManager
private val recommendSeriesViewModel: AudioContentMainRecommendSeriesViewModel by inject()
private lateinit var seriesAdapter: UserProfileSeriesListAdapter
private lateinit var newContentCreatorAdapter: AudioContentMainNewContentCreatorAdapter
private val bannerViewModel: AudioContentMainBannerViewModel by inject()
private lateinit var bannerAdapter: AudioContentMainBannerAdapter
private lateinit var orderListAdapter: AudioContentMainContentAdapter
private val newContentViewModel: AudioContentMainNewContentViewModel by inject()
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentAdapter: AudioContentMainContentAdapter
private val contentRankingViewModel: AudioContentMainRankingViewModel by inject()
private lateinit var contentRankingSortAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var contentRankingAdapter: AudioContentMainRankingAdapter
private val curationViewModel: AudioContentMainCurationViewModel by inject()
private lateinit var curationAdapter: AudioContentMainCurationAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireContext().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
setupView()
bindData()
viewModel.getMain()
curationViewModel.getCurationList()
bannerViewModel.getMainBannerList()
newContentViewModel.getThemeList()
creatorRankViewModel.getCreatorRank()
newContentViewModel.getNewContentOfTheme("전체")
contentRankingViewModel.getContentRanking()
contentRankingViewModel.getContentRankingSortType()
recommendSeriesViewModel.getRecommendSeriesList()
}
private fun setupView() {
@@ -75,33 +108,72 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
binding.llUploadContent.visibility = View.GONE
}
setupNewContentCreator()
setupCreatorRank()
setupRecommendSeries()
setupBanner()
setupOrderList()
setupNewContentTheme()
setupNewContent()
setupContentRankingSortType()
setupContentRanking()
setupCuration()
binding.swipeRefreshLayout.setOnRefreshListener {
binding.swipeRefreshLayout.isRefreshing = false
viewModel.getMain()
binding.llShortPlay.setOnClickListener {
startActivity(
Intent(requireContext(), AudioContentAllByThemeActivity::class.java).apply {
putExtra(Constants.EXTRA_THEME_ID, 11L)
}
)
}
binding.llMorningCall.setOnClickListener {
startActivity(
Intent(requireContext(), AudioContentAllByThemeActivity::class.java).apply {
putExtra(Constants.EXTRA_THEME_ID, 12L)
}
)
}
binding.ivContentKeep.setOnClickListener {
startActivity(
Intent(
requireContext(),
AudioContentBoxActivity::class.java
)
)
}
binding.ivAlarm.setOnClickListener {
startActivity(
Intent(
requireActivity(),
AlarmListActivity::class.java
)
)
}
binding.flSearchChannel.setOnClickListener {
}
}
private fun setupNewContentCreator() {
newContentCreatorAdapter = AudioContentMainNewContentCreatorAdapter {
val intent = Intent(requireContext(), UserProfileActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, it)
startActivity(intent)
}
private fun setupCreatorRank() {
creatorRankAdaptor = ExplorerSectionAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
},
isVisibleRanking = true
)
binding.rvNewContentCreator.layoutManager = LinearLayoutManager(
binding.rvCreatorRank.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContentCreator.addItemDecoration(object : RecyclerView.ItemDecoration() {
binding.rvCreatorRank.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
@@ -113,23 +185,140 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 10.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
outRect.left = 10.7f.dpToPx().toInt()
creatorRankAdaptor.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 10.7f.dpToPx().toInt()
outRect.right = 10.7f.dpToPx().toInt()
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvNewContentCreator.adapter = newContentCreatorAdapter
binding.rvCreatorRank.adapter = creatorRankAdaptor
creatorRankViewModel.creatorRankLiveData.observe(viewLifecycleOwner) {
binding.tvDesc.text = it.desc
binding.tvCreatorRankTitle.text = if (
!it.coloredTitle.isNullOrBlank() &&
!it.color.isNullOrBlank()
) {
val spStr = SpannableString(it.title)
try {
spStr.setSpan(
ForegroundColorSpan(
Color.parseColor("#${it.color}")
),
it.title.indexOf(it.coloredTitle),
it.title.indexOf(it.coloredTitle) + it.coloredTitle.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
spStr
} catch (e: IllegalArgumentException) {
it.title
}
} else {
it.title
}
creatorRankAdaptor.addItems(it.creators)
if (creatorRankAdaptor.itemCount <= 0 && it.creators.isEmpty()) {
binding.llCreatorRank.visibility = View.GONE
binding.rvCreatorRank.visibility = View.GONE
} else {
binding.llCreatorRank.visibility = View.VISIBLE
binding.rvCreatorRank.visibility = View.VISIBLE
}
}
creatorRankViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupRecommendSeries() {
seriesAdapter = UserProfileSeriesListAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
},
isVisibleCreator = true
)
val recyclerView = binding.rvRecommendSeries
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
seriesAdapter.itemCount - 1 -> {
outRect.right = 0
outRect.left = 6.7f.dpToPx().toInt()
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = seriesAdapter
recommendSeriesViewModel.seriesListLiveData.observe(viewLifecycleOwner) {
seriesAdapter.addItems(it)
binding.llRecommendSeries.visibility = if (
seriesAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
recommendSeriesViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
binding.llRecommendSeriesRefresh.setOnClickListener {
seriesAdapter.clear()
recommendSeriesViewModel.getRecommendSeriesList()
}
}
private fun setupBanner() {
@@ -164,6 +353,14 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
)
}
AudioContentBannerType.SERIES -> {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it.seriesId!!)
}
)
}
AudioContentBannerType.LINK -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
}
@@ -190,73 +387,30 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
.setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_9970ff)
ContextCompat.getColor(requireContext(), R.color.color_3bb9f1)
)
.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)
}
)
bannerViewModel.bannerLiveData.observe(viewLifecycleOwner) {
if (bannerAdapter.itemCount <= 0 && it.isEmpty()) {
binding.rvBanner.visibility = View.GONE
binding.indicatorBanner.visibility = View.GONE
} else {
binding.rvBanner.visibility = View.VISIBLE
binding.indicatorBanner.visibility = View.VISIBLE
binding.rvBanner.refreshData(it)
}
)
}
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))
bannerViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupNewContentTheme() {
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
viewModel.getNewContentOfTheme(theme = it)
newContentViewModel.getNewContentOfTheme(theme = it)
}
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
@@ -280,7 +434,7 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
outRect.right = 4f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
newContentThemeAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
@@ -294,9 +448,18 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
})
binding.rvNewContentTheme.adapter = newContentThemeAdapter
newContentViewModel.themeListLiveData.observe(viewLifecycleOwner) {
binding.llNewContent.visibility = View.VISIBLE
newContentThemeAdapter.addItems(it)
}
}
private fun setupNewContent() {
binding.ivNewContentAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentNewAllActivity::class.java))
}
newContentAdapter = AudioContentMainContentAdapter(
onClickItem = {
startActivity(
@@ -335,7 +498,7 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
outRect.right = 6.7f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
newContentAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
@@ -349,6 +512,120 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
})
binding.rvNewContent.adapter = newContentAdapter
newContentViewModel.newContentListLiveData.observe(viewLifecycleOwner) {
newContentAdapter.addItems(it)
}
newContentViewModel.isLoading.observe(viewLifecycleOwner) {
binding.pbNewContent.visibility = if (it) {
View.VISIBLE
} else {
View.GONE
}
}
newContentViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupContentRankingSortType() {
contentRankingSortAdapter = AudioContentMainNewContentThemeAdapter {
contentRankingViewModel.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
contentRankingViewModel.contentRankingSortListLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
contentRankingSortAdapter.addItems(it)
}
}
@SuppressLint("SetTextI18n")
private fun setupContentRanking() {
binding.ivContentRankingAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentRankingAllActivity::class.java))
}
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
contentRankingViewModel.contentRankingLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
binding.tvDate.text = "${it.startDate}~${it.endDate}"
contentRankingAdapter.addItems(it.items)
}
contentRankingViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupCuration() {
@@ -366,6 +643,15 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
putExtra(Constants.EXTRA_USER_ID, it)
}
)
},
onClickCurationMore = { curationId, title ->
startActivity(
Intent(requireContext(), AudioContentCurationActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_ID, curationId)
putExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_TITLE, title)
}
)
}
)
@@ -402,73 +688,50 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
}
}
})
binding.rvCuration.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
curationViewModel.getCurationList()
}
}
})
binding.rvCuration.adapter = curationAdapter
}
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
curationViewModel.curationListLiveData.observe(viewLifecycleOwner) {
if (curationViewModel.page == 2) {
curationAdapter.clear()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.newContentUploadCreatorListLiveData.observe(viewLifecycleOwner) {
newContentCreatorAdapter.addItems(it)
binding.rvNewContentCreator.visibility = if (
newContentCreatorAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
viewModel.bannerLiveData.observe(viewLifecycleOwner) {
if (bannerAdapter.itemCount <= 0 && it.isEmpty()) {
binding.rvBanner.visibility = View.GONE
binding.indicatorBanner.visibility = View.GONE
} else {
binding.rvBanner.visibility = View.VISIBLE
binding.indicatorBanner.visibility = View.VISIBLE
binding.rvBanner.refreshData(it)
}
}
viewModel.orderListLiveData.observe(viewLifecycleOwner) {
orderListAdapter.addItems(it)
binding.llMyStash.visibility = if (
orderListAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
viewModel.newContentListLiveData.observe(viewLifecycleOwner) {
newContentAdapter.addItems(it)
}
viewModel.themeListLiveData.observe(viewLifecycleOwner) {
binding.llNewContent.visibility = View.VISIBLE
newContentThemeAdapter.addItems(it)
}
viewModel.curationListLiveData.observe(viewLifecycleOwner) {
curationAdapter.addItems(it)
binding.rvCuration.visibility = if (
curationAdapter.itemCount <= 0 && it.isEmpty()
) {
binding.rvCuration.visibility = if (curationAdapter.itemCount <= 0 && it.isEmpty()) {
View.GONE
} else {
View.VISIBLE
}
}
curationViewModel.isLoading.observe(viewLifecycleOwner) {
binding.pbCuration.visibility = if (it) {
View.VISIBLE
} else {
View.GONE
}
}
curationViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
}

View File

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

@@ -1,45 +1,63 @@
package kr.co.vividnext.sodalive.audio_content.main
import androidx.annotation.Keep
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>
)
data class GetNewContentUploadCreator(
@Keep
data class ContentCreatorResponse(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
)
@Keep
data class GetAudioContentMainItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("title") val title: String,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: String
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("price") val price: Int,
@SerializedName("duration") val duration: String
)
@Keep
data class GetAudioContentRanking(
@SerializedName("startDate") val startDate: String,
@SerializedName("endDate") val endDate: String,
@SerializedName("items") val items: List<GetAudioContentRankingItem>
)
@Keep
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,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
)
@Keep
data class GetAudioContentCurationResponse(
@SerializedName("curationId") val curationId: Long,
@SerializedName("title") val title: String,
@SerializedName("description") val description: String,
@SerializedName("contents") val audioContents: List<GetAudioContentMainItem>
)
@Keep
data class GetAudioContentBannerResponse(
@SerializedName("type") val type: AudioContentBannerType,
@SerializedName("thumbnailImageUrl") val thumbnailImageUrl: String,
@SerializedName("eventItem") val eventItem: EventItem?,
@SerializedName("creatorId") val creatorId: Long?,
@SerializedName("seriesId") val seriesId: Long?,
@SerializedName("link") val link: String?
)
@@ -51,5 +69,8 @@ enum class AudioContentBannerType {
CREATOR,
@SerializedName("LINK")
LINK
LINK,
@SerializedName("SERIES")
SERIES
}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.audio_content.main
package kr.co.vividnext.sodalive.audio_content.main.curation
import android.annotation.SuppressLint
import android.content.Context
@@ -8,12 +8,16 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainContentAdapter
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentCurationResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainCurationBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainCurationAdapter(
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit,
private val onClickCurationMore: (Long, String) -> Unit
) : RecyclerView.Adapter<AudioContentMainCurationAdapter.ViewHolder>() {
private val items = mutableListOf<GetAudioContentCurationResponse>()
@@ -25,6 +29,7 @@ class AudioContentMainCurationAdapter(
fun bind(item: GetAudioContentCurationResponse) {
binding.tvTitle.text = item.title
binding.tvDesc.text = item.description
binding.ivAll.setOnClickListener { onClickCurationMore(item.curationId, item.title) }
setAudioContentList(item.audioContents)
}
@@ -87,8 +92,11 @@ class AudioContentMainCurationAdapter(
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetAudioContentCurationResponse>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
fun clear() {
this.items.clear()
}
}

View File

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

View File

@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.audio_content.main
package kr.co.vividnext.sodalive.audio_content.main.new_content
import android.annotation.SuppressLint
import android.content.Context
@@ -22,11 +22,15 @@ class AudioContentMainNewContentThemeAdapter(
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(theme: String) {
if (theme == selectedTheme || (selectedTheme == "" && theme == "전체")) {
if (
theme == selectedTheme ||
(selectedTheme == "" && theme == "전체") ||
(selectedTheme == "" && theme == "매출")
) {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_9970ff
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
)
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_9970ff))
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_3bb9f1))
} else {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_777777
@@ -45,7 +49,6 @@ class AudioContentMainNewContentThemeAdapter(
@SuppressLint("NotifyDataSetChanged")
fun addItems(themeList: List<String>) {
this.selectedTheme = ""
this.themeList.clear()
this.themeList.addAll(themeList)
notifyDataSetChanged()

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
package kr.co.vividnext.sodalive.audio_content.main.ranking
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.GetExplorerSectionResponse
class AudioContentMainCreatorRankingViewModel(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _creatorRankLiveData = MutableLiveData<GetExplorerSectionResponse>()
val creatorRankLiveData: LiveData<GetExplorerSectionResponse>
get() = _creatorRankLiveData
fun getCreatorRank() {
compositeDisposable.add(
repository
.getCreatorRank(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_creatorRankLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"인기 크리에이터를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"인기 크리에이터를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
)
}
)
)
}
}

View File

@@ -0,0 +1,66 @@
package kr.co.vividnext.sodalive.audio_content.main.ranking
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.audio_content.main.GetAudioContentRankingItem
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,86 @@
package kr.co.vividnext.sodalive.audio_content.main.ranking
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRanking
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentMainRankingViewModel(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _contentRankingSortListLiveData = MutableLiveData<List<String>>()
val contentRankingSortListLiveData: LiveData<List<String>>
get() = _contentRankingSortListLiveData
private var _contentRankingLiveData = MutableLiveData<GetAudioContentRanking>()
val contentRankingLiveData: LiveData<GetAudioContentRanking>
get() = _contentRankingLiveData
fun getContentRankingSortType() {
compositeDisposable.add(
repository.getContentRankingSortType(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_contentRankingSortListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun getContentRanking(sort: String = "매출") {
compositeDisposable.add(
repository.getContentRanking(
page = 1,
size = 12,
sortType = sort,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_contentRankingLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

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

View File

@@ -0,0 +1,418 @@
package kr.co.vividnext.sodalive.audio_content.main.v2
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.graphics.Typeface
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import coil.load
import coil.transform.RoundedCornersTransformation
import com.google.android.material.tabs.TabLayout
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.v2.alarm.AudioContentMainTabAlarmFragment
import kr.co.vividnext.sodalive.audio_content.main.v2.asmr.AudioContentMainTabAsmrFragment
import kr.co.vividnext.sodalive.audio_content.main.v2.content.AudioContentMainTabContentFragment
import kr.co.vividnext.sodalive.audio_content.main.v2.free.AudioContentMainTabFreeFragment
import kr.co.vividnext.sodalive.audio_content.main.v2.replay.AudioContentMainTabReplayFragment
import kr.co.vividnext.sodalive.audio_content.main.v2.series.AudioContentMainTabSeriesFragment
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerFragment
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentMainBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.mypage.alarm.AlarmListActivity
import kotlin.math.min
enum class AudioContentMainTab {
HOME, SERIES, CONTENT, ALARM, ASMR, REPLAY, FREE;
companion object {
fun fromOrdinal(ordinal: Int): AudioContentMainTab? {
return values().getOrNull(ordinal)
}
}
}
@OptIn(UnstableApi::class)
class AudioContentMainActivity : BaseActivity<ActivityAudioContentMainBinding>(
ActivityAudioContentMainBinding::inflate
) {
private var fontBold: Typeface? = null
private var fontMedium: Typeface? = null
private var startTabPosition: AudioContentMainTab = AudioContentMainTab.SERIES
private var mediaController: MediaController? = null
private val handler = Handler(Looper.getMainLooper())
private val audioContentReceiver = AudioContentReceiver()
override fun onDestroy() {
deInitMiniPlayer()
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
super.onDestroy()
}
private fun showPlayerFragment() {
val playerFragment = AudioContentPlayerFragment(screenWidth, arrayListOf())
playerFragment.show(supportFragmentManager, playerFragment.tag)
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onResume() {
super.onResume()
val intentFilter = IntentFilter(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(audioContentReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(audioContentReceiver, intentFilter)
}
startService(
Intent(this, AudioContentPlayService::class.java).apply {
action = AudioContentPlayService.MusicAction.INIT.name
}
)
}
override fun onPause() {
unregisterReceiver(audioContentReceiver)
super.onPause()
}
override fun setupView() {
startTabPosition = AudioContentMainTab.fromOrdinal(
intent.getIntExtra(
Constants.EXTRA_START_TAB_POSITION,
AudioContentMainTab.SERIES.ordinal
)
) ?: AudioContentMainTab.SERIES
setupToolbar()
loadFont()
setupTabs()
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
if (SharedPreferenceManager.isPlayerServiceRunning) {
initAndVisibleMiniPlayer()
} else {
deInitMiniPlayer()
}
}
private fun setupToolbar() {
val toolbar = binding.toolbar
toolbar.ivContentKeep.setOnClickListener {
startActivity(
Intent(
applicationContext,
AudioContentBoxActivity::class.java
)
)
}
toolbar.ivAlarm.setOnClickListener {
startActivity(
Intent(
applicationContext,
AlarmListActivity::class.java
)
)
}
}
private fun loadFont() {
fontBold = ResourcesCompat.getFont(this, R.font.gmarket_sans_bold)
fontMedium = ResourcesCompat.getFont(this, R.font.gmarket_sans_medium)
}
private fun setupTabs() {
val tabs = binding.tabs
val tabTitles = listOf("", "시리즈", "단편", "모닝콜", "ASMR", "다시듣기", "무료")
for (title in tabTitles) {
tabs.addTab(tabs.newTab().setText(title))
}
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
val selectedTab = AudioContentMainTab.fromOrdinal(tab.position)
if (selectedTab == null || selectedTab == AudioContentMainTab.HOME) finish()
replaceFragment(selectedTab = selectedTab!!)
tab.view.isSelected = true
setTabFont(tab, fontBold)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
tab.view.isSelected = false
setTabFont(tab, fontMedium)
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
tabs.getTabAt(startTabPosition.ordinal)?.let {
it.select()
scrollToTab(tabs, startTabPosition.ordinal)
}
replaceFragment(selectedTab = startTabPosition)
}
private fun scrollToTab(tabLayout: TabLayout, position: Int) {
tabLayout.post {
val layout = tabLayout.getChildAt(0) as ViewGroup
val tabView = layout.getChildAt(position)
// 화면 전체 너비
val parentWidth = tabLayout.width
// 선택한 탭의 중심 좌표
val tabCenterX = tabView.left + tabView.width / 2
// 스크롤 할 위치 = 탭의 중심을 화면 중앙에 배치
val scrollToX = tabCenterX - parentWidth / 2
tabLayout.scrollTo(min(tabView.left, scrollToX), 0)
}
}
private fun replaceFragment(selectedTab: AudioContentMainTab) {
val startFragment = when (selectedTab) {
AudioContentMainTab.CONTENT -> AudioContentMainTabContentFragment()
AudioContentMainTab.ALARM -> AudioContentMainTabAlarmFragment()
AudioContentMainTab.ASMR -> AudioContentMainTabAsmrFragment()
AudioContentMainTab.REPLAY -> AudioContentMainTabReplayFragment()
AudioContentMainTab.FREE -> AudioContentMainTabFreeFragment()
else -> AudioContentMainTabSeriesFragment()
}
supportFragmentManager.beginTransaction()
.replace(
R.id.fl_container,
startFragment
)
.commit()
}
private fun setTabFont(tab: TabLayout.Tab, font: Typeface?) {
(tab.view.getChildAt(1) as? TextView)?.typeface = font
}
private val preferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
// 특정 키에 대한 값이 변경될 때 UI 업데이트
if (key == Constants.PREF_IS_PLAYER_SERVICE_RUNNING) {
if (sharedPreferences.getBoolean(key, false)) {
handler.postDelayed(
{
initAndVisibleMiniPlayer()
},
1500
)
} else {
deInitMiniPlayer()
}
}
}
private fun initAndVisibleMiniPlayer() {
binding.clMiniPlayer.visibility = View.VISIBLE
binding.clMiniPlayer.setOnClickListener { showPlayerFragment() }
binding.ivPlayerStop.setOnClickListener {
startService(
Intent(applicationContext, AudioContentPlayerService::class.java).apply {
action = "STOP_SERVICE"
}
)
}
connectPlayerService()
}
private fun connectPlayerService() {
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
val sessionToken = SessionToken(applicationContext, componentName)
val mediaControllerFuture =
MediaController.Builder(applicationContext, sessionToken).buildAsync()
mediaControllerFuture.addListener(
{
mediaController = mediaControllerFuture.get()
setupMediaController()
updateMediaMetadata(mediaController?.mediaMetadata)
binding.ivPlayerPlayOrPause.setImageResource(
if (mediaController!!.isPlaying) {
R.drawable.ic_player_pause
} else {
R.drawable.ic_player_play
}
)
binding.ivPlayerPlayOrPause.setOnClickListener {
mediaController?.let {
if (it.playWhenReady) {
it.pause()
} else {
it.play()
}
}
}
},
ContextCompat.getMainExecutor(applicationContext)
)
}
private fun updateMediaMetadata(metadata: MediaMetadata?) {
metadata?.let {
binding.tvPlayerTitle.text = it.title
binding.tvPlayerNickname.text = it.artist
binding.ivPlayerCover.load(it.artworkUri) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4f))
}
}
}
private fun setupMediaController() {
if (mediaController == null) {
deInitMiniPlayer()
return
}
mediaController!!.addListener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
updateMediaMetadata(mediaItem?.mediaMetadata)
}
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
binding.ivPlayerPlayOrPause.setImageResource(
if (playWhenReady) {
R.drawable.ic_player_pause
} else {
R.drawable.ic_player_play
}
)
}
})
}
private fun deInitMiniPlayer() {
binding.clMiniPlayer.visibility = View.GONE
mediaController?.release()
mediaController = null
}
inner class AudioContentReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
val title = intent?.getStringExtra(Constants.EXTRA_AUDIO_CONTENT_TITLE)
val nickname = intent?.getStringExtra(Constants.EXTRA_NICKNAME)
val coverImageUrl = intent?.getStringExtra(
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL
)
val isPlaying = intent?.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYING, false)
val isShowing = intent?.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_SHOWING, false)
if (isShowing == true) {
binding.rlMiniPlayer.visibility = View.VISIBLE
if (contentId != null && contentId > 0) {
binding.rlMiniPlayer.setOnClickListener {
startActivity(
Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
)
}
}
if (isPlaying == true) {
binding.ivPlayOrPause.setImageResource(R.drawable.ic_noti_pause)
binding.ivPlayOrPause.setOnClickListener {
startService(
Intent(
this@AudioContentMainActivity,
AudioContentPlayService::class.java
).apply {
action = AudioContentPlayService.MusicAction.PAUSE.name
}
)
}
} else {
binding.ivPlayOrPause.setImageResource(R.drawable.ic_noti_play)
binding.ivPlayOrPause.setOnClickListener {
startService(
Intent(
this@AudioContentMainActivity,
AudioContentPlayService::class.java
).apply {
action = AudioContentPlayService.MusicAction.PLAY.name
}
)
}
}
binding.ivStop.setOnClickListener {
startService(
Intent(
this@AudioContentMainActivity,
AudioContentPlayService::class.java
).apply {
action = AudioContentPlayService.MusicAction.STOP.name
}
)
}
if (!title.isNullOrBlank()) {
binding.tvMiniPlayerTitle.text = title
}
if (!nickname.isNullOrBlank()) {
binding.tvNickname.text = nickname
}
if (!coverImageUrl.isNullOrBlank()) {
binding.ivCover.load(coverImageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
}
} else {
handler.post {
binding.ivPlayOrPause.setImageResource(0)
binding.ivCover.setImageResource(0)
binding.tvMiniPlayerTitle.text = ""
binding.tvNickname.text = ""
binding.rlMiniPlayer.visibility = View.GONE
binding.ivPlayOrPause.setOnClickListener {}
}
}
}
}
}

View File

@@ -0,0 +1,97 @@
package kr.co.vividnext.sodalive.audio_content.main.v2
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.audio_content.main.AudioContentMainContentAdapter
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainCurationBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainContentCurationAdapter(
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentMainContentCurationAdapter.ViewHolder>() {
private val items = mutableListOf<GetContentCurationResponse>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentMainCurationBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetContentCurationResponse) {
binding.tvTitle.text = item.title
binding.ivAll.visibility = View.GONE
binding.tvDesc.visibility = View.GONE
setAudioContentList(item.items)
}
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<GetContentCurationResponse>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,89 @@
package kr.co.vividnext.sodalive.audio_content.main.v2
import android.annotation.SuppressLint
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.audio_content.main.ContentCreatorResponse
import kr.co.vividnext.sodalive.databinding.ItemContentRankCreatorBinding
class ContentRankCreatorAdapter(
private val onClickItem: (Long) -> Unit,
) : RecyclerView.Adapter<ContentRankCreatorAdapter.ViewHolder>() {
private var selectedCreatorId: Long = 0
private val items = mutableListOf<ContentCreatorResponse>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemContentRankCreatorBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(item: ContentCreatorResponse) {
binding.root.setOnClickListener {
if (selectedCreatorId != item.creatorId) {
selectedCreatorId = item.creatorId
onClickItem(item.creatorId)
notifyDataSetChanged()
}
}
binding.tvNickname.text = item.creatorNickname
binding.ivProfile.load(item.creatorProfileImageUrl) {
transformations(CircleCropTransformation())
placeholder(R.drawable.ic_place_holder)
crossfade(true)
}
if (item.creatorId == selectedCreatorId) {
binding.ivBg.setImageResource(R.drawable.bg_round_corner_33_3_transparent_3bb9f1)
binding.ivBg.visibility = View.VISIBLE
binding.tvNickname.setTextColor(
ContextCompat.getColor(
context,
R.color.color_3bb9f1
)
)
} else {
binding.ivBg.setImageResource(0)
binding.ivBg.visibility = View.GONE
binding.tvNickname.setTextColor(
ContextCompat.getColor(
context,
R.color.color_bbbbbb
)
)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemContentRankCreatorBinding.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<ContentCreatorResponse>) {
this.items.addAll(items)
if (this.items.isNotEmpty()) {
this.selectedCreatorId = this.items[0].creatorId
}
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.audio_content.main.v2
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
@Keep
data class GetContentCurationResponse(
@SerializedName("title") val title: String,
@SerializedName("items") val items: List<GetAudioContentMainItem>
)

View File

@@ -0,0 +1,106 @@
package kr.co.vividnext.sodalive.audio_content.main.v2
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.MultiTransformation
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainTabPopularContentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class PopularContentByCreatorAdapter(
private val itemWidth: Int,
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit
) : RecyclerView.Adapter<PopularContentByCreatorAdapter.ViewHolder>() {
private val items = mutableListOf<GetAudioContentRankingItem>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentMainTabPopularContentBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentRankingItem) {
val lp = binding.ivCover.layoutParams as ConstraintLayout.LayoutParams
lp.width = itemWidth
lp.height = itemWidth
binding.ivCover.layoutParams = lp
Glide
.with(context)
.load(item.coverImageUrl)
.apply(
RequestOptions().transform(
MultiTransformation(
CenterCrop(),
RoundedCorners(5.3f.dpToPx().toInt())
)
)
)
.placeholder(R.drawable.bg_black)
.into(binding.ivCover)
Glide
.with(context)
.load(item.creatorProfileImageUrl)
.apply(
RequestOptions().transform(
CircleCrop()
)
)
.placeholder(R.drawable.bg_black)
.into(binding.ivCreator)
binding.tvTitle.text = item.title
binding.tvNickname.text = item.creatorNickname
if (item.price > 0) {
binding.ivCan.visibility = View.VISIBLE
binding.tvCan.text = item.price.moneyFormat()
} else {
binding.ivCan.visibility = View.GONE
binding.tvCan.text = "무료"
}
binding.tvTime.text = item.duration
binding.ivCover.setOnClickListener { onClickItem(item.contentId) }
binding.ivCreator.setOnClickListener { onClickCreator(item.creatorId) }
binding.tvNickname.setOnClickListener { onClickCreator(item.creatorId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemAudioContentMainTabPopularContentBinding.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<GetAudioContentRankingItem>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,388 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.alarm
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
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.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainContentAdapter
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.AudioContentMainContentCurationAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.alarm.all.AlarmContentAllActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
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.FragmentAudioContentMainTabAlarmBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.event_banner.EventBannerAdapter
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
@OptIn(UnstableApi::class)
class AudioContentMainTabAlarmFragment : BaseFragment<FragmentAudioContentMainTabAlarmBinding>(
FragmentAudioContentMainTabAlarmBinding::inflate
) {
private val viewModel: AudioContentMainTabAlarmViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var contentBannerAdapter: AudioContentMainBannerAdapter
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentAdapter: AudioContentMainContentAdapter
private lateinit var curationAdapter: AudioContentMainContentCurationAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
bindData()
viewModel.fetchData()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupContentBanner()
setupNewContentTheme()
setupNewContent()
setupEventBanner()
setupCuration()
}
private fun setupContentBanner() {
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
contentBannerAdapter = 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.SERIES -> {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it.seriesId!!)
}
)
}
AudioContentBannerType.LINK -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
}
}
}
binding
.rvBanner
.layoutParams = layoutParams
binding.rvBanner.apply {
adapter = contentBannerAdapter 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_3bb9f1)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
viewModel.contentBannerLiveData.observe(viewLifecycleOwner) {
if (contentBannerAdapter.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)
}
}
}
private fun setupNewContentTheme() {
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
viewModel.getContentMainAlarm(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
viewModel.themeListLiveData.observe(viewLifecycleOwner) {
binding.llNewContent.visibility = View.VISIBLE
newContentThemeAdapter.addItems(it)
}
}
private fun setupNewContent() {
binding.ivNewContentAll.setOnClickListener {
startActivity(Intent(requireContext(), AlarmContentAllActivity::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
viewModel.newContentListLiveData.observe(viewLifecycleOwner) {
newContentAdapter.addItems(it)
}
}
private fun setupEventBanner() {
val imageSliderLp = binding.eventBannerSlider.layoutParams
imageSliderLp.width = screenWidth
imageSliderLp.height = (screenWidth * 300) / 1000
binding.eventBannerSlider.layoutParams = imageSliderLp
binding.eventBannerSlider.apply {
adapter = EventBannerAdapter(requireContext()) {
if (it.detailImageUrl != null) {
val intent = Intent(requireActivity(), EventDetailActivity::class.java)
intent.putExtra(Constants.EXTRA_EVENT, it)
startActivity(intent)
} else if (!it.link.isNullOrBlank()) {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(it.link)
)
)
}
} as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(800)
}.create()
binding.eventBannerSlider
.setIndicatorView(binding.indicatorEventBanner)
.setIndicatorStyle(IndicatorStyle.ROUND_RECT)
.setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
.setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_3bb9f1)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
viewModel.eventLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.eventBannerSlider.visibility = View.VISIBLE
binding.indicatorEventBanner.visibility = View.VISIBLE
binding.eventBannerSlider.refreshData(it)
} else {
binding.eventBannerSlider.visibility = View.GONE
binding.indicatorEventBanner.visibility = View.GONE
}
}
}
private fun setupCuration() {
curationAdapter = AudioContentMainContentCurationAdapter(
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.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 = 30f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
curationAdapter.itemCount - 1 -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 30f.dpToPx().toInt()
}
else -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
}
}
})
binding.rvCuration.adapter = curationAdapter
viewModel.curationListLiveData.observe(viewLifecycleOwner) {
curationAdapter.addItems(it)
binding.rvCuration.visibility = if (curationAdapter.itemCount <= 0 && it.isEmpty()) {
View.GONE
} else {
View.VISIBLE
}
}
}
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
}
}

View File

@@ -0,0 +1,27 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.alarm
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.ContentType
class AudioContentMainTabAlarmRepository(private val api: AudioContentApi) {
fun getContentMainAlarm(token: String) = api.getContentMainAlarm(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getContentMainAlarmAll(
theme: String,
page: Int,
size: Int,
token: String
) = api.getContentMainAlarmAll(
theme = theme,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
authHeader = token
)
}

View File

@@ -1,16 +1,19 @@
package kr.co.vividnext.sodalive.audio_content.main
package kr.co.vividnext.sodalive.audio_content.main.v2.alarm
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.event.EventItem
class AudioContentMainViewModel(
private val repository: AudioContentRepository
class AudioContentMainTabAlarmViewModel(
private val repository: AudioContentMainTabAlarmRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
@@ -20,50 +23,45 @@ class AudioContentMainViewModel(
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _newContentUploadCreatorListLiveData =
MutableLiveData<List<GetNewContentUploadCreator>>()
val newContentUploadCreatorListLiveData: LiveData<List<GetNewContentUploadCreator>>
get() = _newContentUploadCreatorListLiveData
private var _contentBannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
val contentBannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
get() = _contentBannerLiveData
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>>
private val _eventLiveData = MutableLiveData<List<EventItem>>()
val eventLiveData: LiveData<List<EventItem>>
get() = _eventLiveData
private var _curationListLiveData = MutableLiveData<List<GetContentCurationResponse>>()
val curationListLiveData: LiveData<List<GetContentCurationResponse>>
get() = _curationListLiveData
fun getMain() {
fun fetchData() {
_isLoading.value = true
compositeDisposable.add(
repository.getMain(token = "Bearer ${SharedPreferenceManager.token}")
repository.getContentMainAlarm(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
val themeList = listOf("전체").union(data.themeList).toList()
_contentBannerLiveData.value = data.contentBannerList
val themeList = listOf("전체").union(data.alarmThemeList).toList()
_themeListLiveData.value = themeList
_newContentListLiveData.value = data.newAlarmContentList
_eventLiveData.value = data.eventBannerList.eventList
_curationListLiveData.value = data.curationList
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
@@ -85,14 +83,18 @@ class AudioContentMainViewModel(
)
}
fun getNewContentOfTheme(theme: String) {
fun getContentMainAlarm(selectedTheme: String) {
_isLoading.value = true
compositeDisposable.add(
repository.getNewContentOfTheme(
theme = if (theme == "전체") {
repository.getContentMainAlarmAll(
theme = if (selectedTheme == "전체") {
""
} else {
theme
selectedTheme
},
page = 1,
size = 10,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
@@ -100,7 +102,7 @@ class AudioContentMainViewModel(
.subscribe(
{
if (it.success && it.data != null) {
_newContentListLiveData.value = it.data!!
_newContentListLiveData.value = it.data.items
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
@@ -110,8 +112,11 @@ class AudioContentMainViewModel(
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}

View File

@@ -0,0 +1,25 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.alarm
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse
import kr.co.vividnext.sodalive.settings.event.GetEventResponse
@Keep
data class GetContentMainTabAlarmResponse(
@SerializedName("contentBannerList")
val contentBannerList: List<GetAudioContentBannerResponse>,
@SerializedName("alarmThemeList")
val alarmThemeList: List<String>,
@SerializedName("newAlarmContentList")
val newAlarmContentList: List<GetAudioContentMainItem>,
@SerializedName("rankAlarmContentList")
val rankAlarmContentList: List<GetAudioContentRankingItem>,
@SerializedName("eventBannerList")
val eventBannerList: GetEventResponse,
@SerializedName("curationList")
val curationList: List<GetContentCurationResponse>
)

View File

@@ -0,0 +1,162 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.alarm.all
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAlarmContentAllBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
@OptIn(UnstableApi::class)
class AlarmContentAllActivity : BaseActivity<ActivityAlarmContentAllBinding>(
ActivityAlarmContentAllBinding::inflate
) {
private val viewModel: AlarmContentAllViewModel 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.getContentMainAlarmAll()
newContentThemeAdapter.addItems(listOf("전체", "모닝콜", "슬립콜", "알람"))
}
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() {
val spanCount = 3
val spacing = 40
newContentAdapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
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, spanCount)
binding.rvContent.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
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.getContentMainAlarmAll()
}
}
})
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.newContentListLiveData.observe(this) {
newContentAdapter.addItems(it)
}
viewModel.totalCountLiveData.observe(this) {
binding.tvTotalCount.text = "$it"
}
}
}

View File

@@ -0,0 +1,93 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.alarm.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.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.v2.alarm.AudioContentMainTabAlarmRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AlarmContentAllViewModel(
private val repository: AudioContentMainTabAlarmRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _newContentListLiveData
private var _totalCountLiveData = MutableLiveData<Int>()
val totalCountLiveData: LiveData<Int>
get() = _totalCountLiveData
private var isLast = false
private var page = 1
private val size = 10
private var selectedTheme = ""
fun getContentMainAlarmAll() {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getContentMainAlarmAll(
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) {
val data = it.data
_totalCountLiveData.value = data.totalCount
if (data.items.isNotEmpty()) {
page += 1
_newContentListLiveData.value = data.items
} 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 selectTheme(theme: String) {
isLast = false
page = 1
selectedTheme = theme
getContentMainAlarmAll()
}
}

View File

@@ -0,0 +1,116 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.asmr
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllViewModel
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.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAsmrNewContentAllBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import org.koin.android.ext.android.inject
@OptIn(UnstableApi::class)
class AsmrNewContentAllActivity : BaseActivity<ActivityAsmrNewContentAllBinding>(
ActivityAsmrNewContentAllBinding::inflate
) {
private val viewModel: AudioContentNewAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var newContentAdapter: AudioContentNewAllAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.selectTheme(theme = "ASMR", isFree = false)
}
@SuppressLint("SetTextI18n")
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "새로운 ASMR"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.tvNotice.text = "※ 최근 2주간 등록된 새로운 ASMR 입니다."
setupNewContent()
}
private fun setupNewContent() {
val spanCount = 3
val spacing = 40
newContentAdapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
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, spanCount)
binding.rvContent.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
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.newContentListLiveData.observe(this) {
newContentAdapter.addItems(it)
}
viewModel.newContentTotalCountLiveData.observe(this) {
binding.tvTotalCount.text = "$it"
}
}
}

View File

@@ -0,0 +1,449 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.asmr
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
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.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainContentAdapter
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.AudioContentMainContentCurationAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.ContentRankCreatorAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.PopularContentByCreatorAdapter
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainTabAsmrBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.event_banner.EventBannerAdapter
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
@OptIn(UnstableApi::class)
class AudioContentMainTabAsmrFragment : BaseFragment<FragmentAudioContentMainTabAsmrBinding>(
FragmentAudioContentMainTabAsmrBinding::inflate
) {
private val viewModel: AudioContentMainTabAsmrViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var contentBannerAdapter: AudioContentMainBannerAdapter
private lateinit var newContentAdapter: AudioContentMainContentAdapter
private lateinit var curationAdapter: AudioContentMainContentCurationAdapter
private lateinit var contentRankCreatorAdapter: ContentRankCreatorAdapter
private lateinit var popularContentByCreatorAdapter: PopularContentByCreatorAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
bindData()
viewModel.fetchData()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupContentBanner()
setupNewContent()
setupPopularContentCreator()
setupPopularContentByCreator()
setupEventBanner()
setupCuration()
}
private fun setupContentBanner() {
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
contentBannerAdapter = 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.SERIES -> {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it.seriesId!!)
}
)
}
AudioContentBannerType.LINK -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
}
}
}
binding
.rvBanner
.layoutParams = layoutParams
binding.rvBanner.apply {
adapter = contentBannerAdapter 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_3bb9f1)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
viewModel.contentBannerLiveData.observe(viewLifecycleOwner) {
if (contentBannerAdapter.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)
}
}
}
private fun setupNewContent() {
binding.ivNewContentAll.setOnClickListener {
startActivity(
Intent(requireContext(), AsmrNewContentAllActivity::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
viewModel.newContentListLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llNewContent.visibility = View.VISIBLE
newContentAdapter.addItems(it)
} else {
binding.llNewContent.visibility = View.GONE
}
}
}
private fun setupEventBanner() {
val imageSliderLp = binding.eventBannerSlider.layoutParams
imageSliderLp.width = screenWidth
imageSliderLp.height = (screenWidth * 300) / 1000
binding.eventBannerSlider.layoutParams = imageSliderLp
binding.eventBannerSlider.apply {
adapter = EventBannerAdapter(requireContext()) {
if (it.detailImageUrl != null) {
val intent = Intent(requireActivity(), EventDetailActivity::class.java)
intent.putExtra(Constants.EXTRA_EVENT, it)
startActivity(intent)
} else if (!it.link.isNullOrBlank()) {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(it.link)
)
)
}
} as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(800)
}.create()
binding.eventBannerSlider
.setIndicatorView(binding.indicatorEventBanner)
.setIndicatorStyle(IndicatorStyle.ROUND_RECT)
.setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
.setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_3bb9f1)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
viewModel.eventLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.eventBannerSlider.visibility = View.VISIBLE
binding.indicatorEventBanner.visibility = View.VISIBLE
binding.eventBannerSlider.refreshData(it)
} else {
binding.eventBannerSlider.visibility = View.GONE
binding.indicatorEventBanner.visibility = View.GONE
}
}
}
private fun setupCuration() {
curationAdapter = AudioContentMainContentCurationAdapter(
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.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 = 30f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
curationAdapter.itemCount - 1 -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 30f.dpToPx().toInt()
}
else -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
}
}
})
binding.rvCuration.adapter = curationAdapter
viewModel.curationListLiveData.observe(viewLifecycleOwner) {
curationAdapter.addItems(it)
binding.rvCuration.visibility = if (curationAdapter.itemCount <= 0 && it.isEmpty()) {
View.GONE
} else {
View.VISIBLE
}
}
}
private fun setupPopularContentCreator() {
contentRankCreatorAdapter = ContentRankCreatorAdapter {
binding.llNoItems.visibility = View.VISIBLE
binding.rvRankingSalesCount.visibility = View.GONE
viewModel.getPopularContentByCreator(it)
}
binding.rvRankingCreator.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvRankingCreator.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 = 11f.dpToPx().toInt()
}
contentRankCreatorAdapter.itemCount - 1 -> {
outRect.left = 11f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 11f.dpToPx().toInt()
outRect.right = 11f.dpToPx().toInt()
}
}
}
})
binding.rvRankingCreator.adapter = contentRankCreatorAdapter
viewModel.contentCreatorListLiveData.observe(viewLifecycleOwner) {
contentRankCreatorAdapter.addItems(it)
if (contentRankCreatorAdapter.itemCount <= 0 && it.isEmpty()) {
binding.llCreatorContentRanking.visibility = View.GONE
} else {
binding.llCreatorContentRanking.visibility = View.VISIBLE
}
}
}
private fun setupPopularContentByCreator() {
popularContentByCreatorAdapter = PopularContentByCreatorAdapter(
itemWidth = ((screenWidth - 13.3f.dpToPx() * 3) / 2).toInt(),
onClickItem = { contentId ->
startActivity(
Intent(requireActivity(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
)
},
onClickCreator = { creatorId ->
startActivity(
Intent(requireActivity(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, creatorId)
}
)
}
)
val recyclerView = binding.rvRankingSalesCount
recyclerView.layoutManager = GridLayoutManager(requireContext(), 2)
recyclerView.addItemDecoration(
GridSpacingItemDecoration(
2,
13.3f.dpToPx().toInt(),
false
)
)
recyclerView.adapter = popularContentByCreatorAdapter
viewModel.salesCountRankContentListLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llNoItems.visibility = View.GONE
recyclerView.visibility = View.VISIBLE
popularContentByCreatorAdapter.addItems(it)
} else {
binding.llNoItems.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
}
}
}
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
}
}

View File

@@ -0,0 +1,23 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.asmr
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.ContentType
class AudioContentMainTabAsmrRepository(private val api: AudioContentApi) {
fun getContentMainAsmr(token: String) = api.getContentMainAsmr(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getPopularContentByCreator(
creatorId: Long,
token: String
) = api.getPopularAsmrContentByCreator(
creatorId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
}

View File

@@ -0,0 +1,124 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.asmr
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.main.ContentCreatorResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.event.EventItem
class AudioContentMainTabAsmrViewModel(
private val repository: AudioContentMainTabAsmrRepository
) : 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 _contentBannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
val contentBannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
get() = _contentBannerLiveData
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _newContentListLiveData
private val _contentCreatorListLiveData = MutableLiveData<List<ContentCreatorResponse>>()
val contentCreatorListLiveData: LiveData<List<ContentCreatorResponse>>
get() = _contentCreatorListLiveData
private val _salesCountRankContentListLiveData =
MutableLiveData<List<GetAudioContentRankingItem>>()
val salesCountRankContentListLiveData: LiveData<List<GetAudioContentRankingItem>>
get() = _salesCountRankContentListLiveData
private val _eventLiveData = MutableLiveData<List<EventItem>>()
val eventLiveData: LiveData<List<EventItem>>
get() = _eventLiveData
private var _curationListLiveData = MutableLiveData<List<GetContentCurationResponse>>()
val curationListLiveData: LiveData<List<GetContentCurationResponse>>
get() = _curationListLiveData
fun fetchData() {
_isLoading.value = true
compositeDisposable.add(
repository.getContentMainAsmr(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val data = it.data
_contentBannerLiveData.value = data.contentBannerList
_newContentListLiveData.value = data.newAsmrContentList
_contentCreatorListLiveData.value = data.creatorList
_salesCountRankContentListLiveData.value =
data.salesCountRankContentList
_eventLiveData.value = data.eventBannerList.eventList
_curationListLiveData.value = data.curationList
} 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 getPopularContentByCreator(creatorId: Long) {
_isLoading.value = true
compositeDisposable.add(
repository.getPopularContentByCreator(
creatorId = creatorId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
_salesCountRankContentListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@@ -0,0 +1,26 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.asmr
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.ContentCreatorResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse
import kr.co.vividnext.sodalive.settings.event.GetEventResponse
@Keep
data class GetContentMainTabAsmrResponse(
@SerializedName("contentBannerList")
val contentBannerList: List<GetAudioContentBannerResponse>,
@SerializedName("newAsmrContentList")
val newAsmrContentList: List<GetAudioContentMainItem>,
@SerializedName("creatorList")
val creatorList: List<ContentCreatorResponse>,
@SerializedName("salesCountRankContentList")
val salesCountRankContentList: List<GetAudioContentRankingItem>,
@SerializedName("eventBannerList")
val eventBannerList: GetEventResponse,
@SerializedName("curationList")
val curationList: List<GetContentCurationResponse>
)

View File

@@ -0,0 +1,666 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.content
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
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.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllActivity
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainContentAdapter
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.AudioContentMainContentCurationAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.ContentRankCreatorAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.PopularContentByCreatorAdapter
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainTabContentBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.event_banner.EventBannerAdapter
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
@OptIn(UnstableApi::class)
class AudioContentMainTabContentFragment : BaseFragment<FragmentAudioContentMainTabContentBinding>(
FragmentAudioContentMainTabContentBinding::inflate
) {
private val viewModel: AudioContentMainTabContentViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var contentBannerAdapter: AudioContentMainBannerAdapter
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentAdapter: AudioContentMainContentAdapter
private lateinit var contentRankingSortAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var contentRankingAdapter: AudioContentMainRankingAdapter
private lateinit var contentRankCreatorAdapter: ContentRankCreatorAdapter
private lateinit var curationAdapter: AudioContentMainContentCurationAdapter
private lateinit var popularContentByCreatorAdapter: PopularContentByCreatorAdapter
private lateinit var contentTagAdapter: AudioContentMainTabContentTagAdapter
private lateinit var contentByTagAdapter: AudioContentNewAllAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
bindData()
viewModel.fetchData()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupContentBanner()
setupNewContentTheme()
setupNewContent()
setupContentRankingSortType()
setupContentRanking()
setupEventBanner()
setupPopularContentCreator()
setupPopularContentByCreator()
setupContentTag()
setupContentByTag()
setupCuration()
}
private fun setupContentBanner() {
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
contentBannerAdapter = 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.SERIES -> {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it.seriesId!!)
}
)
}
AudioContentBannerType.LINK -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
}
}
}
binding
.rvBanner
.layoutParams = layoutParams
binding.rvBanner.apply {
adapter = contentBannerAdapter 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_3bb9f1)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
viewModel.contentBannerLiveData.observe(viewLifecycleOwner) {
if (contentBannerAdapter.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)
}
}
}
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
viewModel.themeListLiveData.observe(viewLifecycleOwner) {
binding.llNewContent.visibility = View.VISIBLE
newContentThemeAdapter.addItems(it)
}
}
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
viewModel.newContentListLiveData.observe(viewLifecycleOwner) {
newContentAdapter.addItems(it)
}
}
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
viewModel.contentRankingSortListLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
contentRankingSortAdapter.addItems(it)
}
}
@SuppressLint("SetTextI18n")
private fun setupContentRanking() {
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
viewModel.contentRankingLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
contentRankingAdapter.addItems(it)
}
}
private fun setupEventBanner() {
val imageSliderLp = binding.eventBannerSlider.layoutParams
imageSliderLp.width = screenWidth
imageSliderLp.height = (screenWidth * 300) / 1000
binding.eventBannerSlider.layoutParams = imageSliderLp
binding.eventBannerSlider.apply {
adapter = EventBannerAdapter(requireContext()) {
if (it.detailImageUrl != null) {
val intent = Intent(requireActivity(), EventDetailActivity::class.java)
intent.putExtra(Constants.EXTRA_EVENT, it)
startActivity(intent)
} else if (!it.link.isNullOrBlank()) {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(it.link)
)
)
}
} as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(800)
}.create()
binding.eventBannerSlider
.setIndicatorView(binding.indicatorEventBanner)
.setIndicatorStyle(IndicatorStyle.ROUND_RECT)
.setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
.setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_3bb9f1)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
viewModel.eventLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.eventBannerSlider.visibility = View.VISIBLE
binding.indicatorEventBanner.visibility = View.VISIBLE
binding.eventBannerSlider.refreshData(it)
} else {
binding.eventBannerSlider.visibility = View.GONE
binding.indicatorEventBanner.visibility = View.GONE
}
}
}
private fun setupPopularContentCreator() {
contentRankCreatorAdapter = ContentRankCreatorAdapter {
binding.llNoItems.visibility = View.VISIBLE
binding.rvRankingSalesCount.visibility = View.GONE
viewModel.getPopularContentByCreator(it)
}
binding.rvRankingCreator.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvRankingCreator.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 = 11f.dpToPx().toInt()
}
contentRankCreatorAdapter.itemCount - 1 -> {
outRect.left = 11f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 11f.dpToPx().toInt()
outRect.right = 11f.dpToPx().toInt()
}
}
}
})
binding.rvRankingCreator.adapter = contentRankCreatorAdapter
viewModel.contentRankCreatorListLiveData.observe(viewLifecycleOwner) {
contentRankCreatorAdapter.addItems(it)
if (contentRankCreatorAdapter.itemCount <= 0 && it.isEmpty()) {
binding.llCreatorContentRanking.visibility = View.GONE
} else {
binding.llCreatorContentRanking.visibility = View.VISIBLE
}
}
}
private fun setupPopularContentByCreator() {
popularContentByCreatorAdapter = PopularContentByCreatorAdapter(
itemWidth = ((screenWidth - 13.3f.dpToPx() * 3) / 2).toInt(),
onClickItem = { contentId ->
startActivity(
Intent(requireActivity(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
)
},
onClickCreator = { creatorId ->
startActivity(
Intent(requireActivity(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, creatorId)
}
)
}
)
val recyclerView = binding.rvRankingSalesCount
recyclerView.layoutManager = GridLayoutManager(requireContext(), 2)
recyclerView.addItemDecoration(
GridSpacingItemDecoration(
2,
13.3f.dpToPx().toInt(),
false
)
)
recyclerView.adapter = popularContentByCreatorAdapter
viewModel.salesCountRankContentListLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llNoItems.visibility = View.GONE
recyclerView.visibility = View.VISIBLE
popularContentByCreatorAdapter.addItems(it)
} else {
binding.llNoItems.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
}
}
}
private fun setupContentTag() {
val spanCount = 4
val spacing = 6f.dpToPx()
contentTagAdapter = AudioContentMainTabContentTagAdapter {
viewModel.getRecommendContentByTag(it)
}
val recyclerView = binding.rvRecommendContentTag
recyclerView.layoutManager = GridLayoutManager(requireContext(), spanCount)
recyclerView.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing.toInt(), false))
recyclerView.adapter = contentTagAdapter
viewModel.tagListLiveData.observe(viewLifecycleOwner) {
contentTagAdapter.addItems(it)
if (
contentTagAdapter.itemCount <= 0 ||
!SharedPreferenceManager.isAdultContentVisible ||
!SharedPreferenceManager.isAuth
) {
binding.llRecommendContentByTag.visibility = View.GONE
} else {
binding.llRecommendContentByTag.visibility = View.VISIBLE
}
}
}
private fun setupContentByTag() {
val spanCount = 3
val horizontalSpacing = 13.3f.dpToPx().toInt()
val verticalSpacing = 26.7f.dpToPx().toInt()
val itemWidth = (screenWidth - horizontalSpacing * (spanCount + 1)) / spanCount
contentByTagAdapter = AudioContentNewAllAdapter(
itemWidth = itemWidth,
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)
}
)
}
)
val recyclerView = binding.rvRecommendContent
recyclerView.layoutManager = GridLayoutManager(requireContext(), spanCount)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
outRect.left = horizontalSpacing / 2
outRect.right = horizontalSpacing / 2
outRect.top = verticalSpacing / 2
outRect.bottom = verticalSpacing / 2
}
})
recyclerView.adapter = contentByTagAdapter
viewModel.tagCurationContentListLiveData.observe(viewLifecycleOwner) {
contentByTagAdapter.clear()
contentByTagAdapter.addItems(it)
}
}
private fun setupCuration() {
curationAdapter = AudioContentMainContentCurationAdapter(
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.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 = 30f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
curationAdapter.itemCount - 1 -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 30f.dpToPx().toInt()
}
else -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
}
}
})
binding.rvCuration.adapter = curationAdapter
viewModel.curationListLiveData.observe(viewLifecycleOwner) {
curationAdapter.addItems(it)
binding.rvCuration.visibility = if (curationAdapter.itemCount <= 0 && it.isEmpty()) {
View.GONE
} else {
View.VISIBLE
}
}
}
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
}
}

View File

@@ -0,0 +1,49 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.content
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.ContentType
class AudioContentMainTabContentRepository(private val api: AudioContentApi) {
fun getContentMainContent(token: String) = api.getContentMainContent(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getNewContentOfTheme(theme: String, token: String) = api.getContentMainNewContentOfTheme(
theme = theme,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getContentRanking(
sortType: String = "매출",
token: String
) = api.getDailyContentRanking(
sortType = sortType,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getPopularContentByCreator(
creatorId: Long,
token: String
) = api.getContentMainContentPopularContentByCreator(
creatorId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getRecommendedContentByTag(
tag: String,
token: String
) = api.getRecommendedContentByTag(
tag = tag,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
}

View File

@@ -0,0 +1,77 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.content
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.ItemContentMainTabContentTagBinding
class AudioContentMainTabContentTagAdapter(
private val onClick: (String) -> Unit
) : RecyclerView.Adapter<AudioContentMainTabContentTagAdapter.ViewHolder>() {
private val tagList = mutableListOf<String>()
private var selectedTag = ""
inner class ViewHolder(
private val context: Context,
private val binding: ItemContentMainTabContentTagBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(tag: String) {
if (tag == selectedTag) {
binding.tvTag.setBackgroundResource(
R.drawable.bg_round_corner_2_6_transparent_3bb9f1
)
binding.tvTag.setTextColor(
ContextCompat.getColor(context, R.color.color_3bb9f1)
)
} else {
binding.tvTag.setBackgroundResource(
R.drawable.bg_round_corner_2_6_transparent_777777
)
binding.tvTag.setTextColor(
ContextCompat.getColor(context, R.color.color_777777)
)
}
binding.tvTag.text = tag
binding.tvTag.setOnClickListener {
selectedTag = tag
onClick(tag)
notifyDataSetChanged()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemContentMainTabContentTagBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = tagList.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(tagList[position])
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(tagList: List<String>) {
this.tagList.clear()
this.tagList.addAll(tagList)
if (tagList.isNotEmpty()) {
selectedTag = tagList[0]
}
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,259 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.content
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.main.ContentCreatorResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.event.EventItem
class AudioContentMainTabContentViewModel(
private val repository: AudioContentMainTabContentRepository
) : 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 _contentBannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
val contentBannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
get() = _contentBannerLiveData
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _newContentListLiveData
private var _themeListLiveData = MutableLiveData<List<String>>()
val themeListLiveData: LiveData<List<String>>
get() = _themeListLiveData
private var _contentRankingSortListLiveData = MutableLiveData<List<String>>()
val contentRankingSortListLiveData: LiveData<List<String>>
get() = _contentRankingSortListLiveData
private var _contentRankingLiveData = MutableLiveData<List<GetAudioContentRankingItem>>()
val contentRankingLiveData: LiveData<List<GetAudioContentRankingItem>>
get() = _contentRankingLiveData
private val _eventLiveData = MutableLiveData<List<EventItem>>()
val eventLiveData: LiveData<List<EventItem>>
get() = _eventLiveData
private var _curationListLiveData = MutableLiveData<List<GetContentCurationResponse>>()
val curationListLiveData: LiveData<List<GetContentCurationResponse>>
get() = _curationListLiveData
private val _contentRankCreatorListLiveData = MutableLiveData<List<ContentCreatorResponse>>()
val contentRankCreatorListLiveData: LiveData<List<ContentCreatorResponse>>
get() = _contentRankCreatorListLiveData
private val _salesCountRankContentListLiveData =
MutableLiveData<List<GetAudioContentRankingItem>>()
val salesCountRankContentListLiveData: LiveData<List<GetAudioContentRankingItem>>
get() = _salesCountRankContentListLiveData
private val _tagListLiveData = MutableLiveData<List<String>>()
val tagListLiveData: LiveData<List<String>>
get() = _tagListLiveData
private val _tagCurationContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val tagCurationContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _tagCurationContentListLiveData
fun fetchData() {
_isLoading.value = true
compositeDisposable.add(
repository.getContentMainContent(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val data = it.data
_contentBannerLiveData.value = data.bannerList
val themeList = listOf("전체").union(data.contentThemeList).toList()
_themeListLiveData.value = themeList
_newContentListLiveData.value = data.newContentList
_contentRankingSortListLiveData.value = data.rankSortTypeList
_contentRankingLiveData.value = data.rankContentList
_eventLiveData.value = data.eventBannerList.eventList
_contentRankCreatorListLiveData.value = data.contentRankCreatorList
_salesCountRankContentListLiveData.value =
data.salesCountRankContentList
_curationListLiveData.value = data.curationList
_tagListLiveData.value = data.tagList
_tagCurationContentListLiveData.value = data.tagCurationContentList
} 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) {
_isLoading.value = true
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(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_isLoading.value = false
}
)
)
}
fun getContentRanking(sort: String = "매출") {
_isLoading.value = true
compositeDisposable.add(
repository.getContentRanking(
sortType = sort,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_contentRankingLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_isLoading.value = false
}
)
)
}
fun getPopularContentByCreator(creatorId: Long) {
_isLoading.value = true
compositeDisposable.add(
repository.getPopularContentByCreator(
creatorId = creatorId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
_salesCountRankContentListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun getRecommendContentByTag(tag: String) {
_isLoading.value = true
compositeDisposable.add(
repository.getRecommendedContentByTag(
tag = tag,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_tagCurationContentListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_isLoading.value = false
}
)
)
}
}

View File

@@ -0,0 +1,25 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.content
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.ContentCreatorResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse
import kr.co.vividnext.sodalive.settings.event.GetEventResponse
@Keep
data class GetContentMainTabContentResponse(
@SerializedName("bannerList") val bannerList: List<GetAudioContentBannerResponse>,
@SerializedName("contentThemeList") val contentThemeList: List<String>,
@SerializedName("newContentList") val newContentList: List<GetAudioContentMainItem>,
@SerializedName("rankSortTypeList") val rankSortTypeList: List<String>,
@SerializedName("rankContentList") val rankContentList: List<GetAudioContentRankingItem>,
@SerializedName("contentRankCreatorList") val contentRankCreatorList: List<ContentCreatorResponse>,
@SerializedName("salesCountRankContentList") val salesCountRankContentList: List<GetAudioContentRankingItem>,
@SerializedName("eventBannerList") val eventBannerList: GetEventResponse,
@SerializedName("tagList") val tagList: List<String>,
@SerializedName("tagCurationContentList") val tagCurationContentList: List<GetAudioContentMainItem>,
@SerializedName("curationList") val curationList: List<GetContentCurationResponse>
)

View File

@@ -0,0 +1,591 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.free
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
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.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllActivity
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainContentAdapter
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.AudioContentMainContentCurationAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.ContentRankCreatorAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.PopularContentByCreatorAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.free.introduce_creator.IntroduceCreatorActivity
import kr.co.vividnext.sodalive.audio_content.main.v2.series.new_series.AudioContentMainNewSeriesAdapter
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainTabFreeBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
@OptIn(UnstableApi::class)
class AudioContentMainTabFreeFragment : BaseFragment<FragmentAudioContentMainTabFreeBinding>(
FragmentAudioContentMainTabFreeBinding::inflate
) {
private val viewModel: AudioContentMainTabFreeViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var contentBannerAdapter: AudioContentMainBannerAdapter
private lateinit var introduceCreatorAdapter: AudioContentMainContentAdapter
private lateinit var recommendSeriesAdapter: AudioContentMainNewSeriesAdapter
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentAdapter: AudioContentMainContentAdapter
private lateinit var curationAdapter: AudioContentMainContentCurationAdapter
private lateinit var contentRankCreatorAdapter: ContentRankCreatorAdapter
private lateinit var popularContentByCreatorAdapter: PopularContentByCreatorAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
bindData()
viewModel.fetchData()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupContentBanner()
setupIntroduceCreator()
setupRecommendSeries()
setupNewContentTheme()
setupNewContent()
setupPopularContentCreator()
setupPopularContentByCreator()
setupCuration()
}
private fun setupContentBanner() {
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
contentBannerAdapter = 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.SERIES -> {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it.seriesId!!)
}
)
}
AudioContentBannerType.LINK -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
}
}
}
binding
.rvBanner
.layoutParams = layoutParams
binding.rvBanner.apply {
adapter = contentBannerAdapter 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_3bb9f1)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
viewModel.contentBannerLiveData.observe(viewLifecycleOwner) {
if (contentBannerAdapter.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)
}
}
}
private fun setupIntroduceCreator() {
binding.ivIntroduceCreatorAll.setOnClickListener {
startActivity(
Intent(requireContext(), IntroduceCreatorActivity::class.java)
)
}
introduceCreatorAdapter = 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)
}
)
}
)
val recyclerView = binding.rvIntroduceCreator
recyclerView.layoutManager = LinearLayoutManager(
context,
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()
}
introduceCreatorAdapter.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 = introduceCreatorAdapter
viewModel.introduceCreatorLiveData.observe(viewLifecycleOwner) {
if (it.items.isNotEmpty()) {
binding.llIntroduceCreator.visibility = View.VISIBLE
binding.tvIntroduceCreator.text = it.title
introduceCreatorAdapter.addItems(it.items)
} else {
binding.llIntroduceCreator.visibility = View.GONE
}
}
}
private fun setupRecommendSeries() {
recommendSeriesAdapter = AudioContentMainNewSeriesAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
val recyclerView = binding.rvRecommendSeries
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
recommendSeriesAdapter.itemCount - 1 -> {
outRect.right = 0
outRect.left = 6.7f.dpToPx().toInt()
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = recommendSeriesAdapter
viewModel.recommendSeriesListLiveData.observe(viewLifecycleOwner) {
recommendSeriesAdapter.addItems(it)
binding.llRecommendSeries.visibility = if (it.isNotEmpty()) {
View.VISIBLE
} else {
View.GONE
}
}
}
private fun setupNewContentTheme() {
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
viewModel.getNewFreeContentOfTheme(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
viewModel.themeListLiveData.observe(viewLifecycleOwner) {
binding.llNewContent.visibility = View.VISIBLE
newContentThemeAdapter.addItems(it)
}
}
private fun setupNewContent() {
binding.ivNewContentAll.setOnClickListener {
startActivity(
Intent(
requireContext(),
AudioContentNewAllActivity::class.java
).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, true)
}
)
}
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
viewModel.newContentListLiveData.observe(viewLifecycleOwner) {
newContentAdapter.addItems(it)
}
}
private fun setupPopularContentCreator() {
contentRankCreatorAdapter = ContentRankCreatorAdapter {
binding.llNoItems.visibility = View.VISIBLE
binding.rvRankingPlayCount.visibility = View.GONE
viewModel.getPopularContentByCreator(it)
}
binding.rvRankingCreator.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvRankingCreator.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 = 11f.dpToPx().toInt()
}
contentRankCreatorAdapter.itemCount - 1 -> {
outRect.left = 11f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 11f.dpToPx().toInt()
outRect.right = 11f.dpToPx().toInt()
}
}
}
})
binding.rvRankingCreator.adapter = contentRankCreatorAdapter
viewModel.contentCreatorListLiveData.observe(viewLifecycleOwner) {
contentRankCreatorAdapter.addItems(it)
if (contentRankCreatorAdapter.itemCount <= 0 && it.isEmpty()) {
binding.llCreatorContentRanking.visibility = View.GONE
} else {
binding.llCreatorContentRanking.visibility = View.VISIBLE
}
}
}
private fun setupPopularContentByCreator() {
popularContentByCreatorAdapter = PopularContentByCreatorAdapter(
itemWidth = ((screenWidth - 13.3f.dpToPx() * 3) / 2).toInt(),
onClickItem = { contentId ->
startActivity(
Intent(requireActivity(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
)
},
onClickCreator = { creatorId ->
startActivity(
Intent(requireActivity(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, creatorId)
}
)
}
)
val recyclerView = binding.rvRankingPlayCount
recyclerView.layoutManager = GridLayoutManager(requireContext(), 2)
recyclerView.addItemDecoration(
GridSpacingItemDecoration(
2,
13.3f.dpToPx().toInt(),
false
)
)
recyclerView.adapter = popularContentByCreatorAdapter
viewModel.playCountRankContentListLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llNoItems.visibility = View.GONE
recyclerView.visibility = View.VISIBLE
popularContentByCreatorAdapter.addItems(it)
} else {
binding.llNoItems.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
}
}
}
private fun setupCuration() {
curationAdapter = AudioContentMainContentCurationAdapter(
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.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 = 30f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
curationAdapter.itemCount - 1 -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 30f.dpToPx().toInt()
}
else -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
}
}
})
binding.rvCuration.adapter = curationAdapter
viewModel.curationListLiveData.observe(viewLifecycleOwner) {
curationAdapter.addItems(it)
binding.rvCuration.visibility = if (curationAdapter.itemCount <= 0 && it.isEmpty()) {
View.GONE
} else {
View.VISIBLE
}
}
}
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
}
}

View File

@@ -0,0 +1,45 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.free
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.ContentType
class AudioContentMainTabFreeRepository(private val api: AudioContentApi) {
fun getContentMainFree(token: String) = api.getContentMainFree(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getIntroduceCreatorList(page: Int, size: Int, token: String) = api.getIntroduceCreatorList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
authHeader = token
)
fun getNewContentOfTheme(
theme: String,
page: Int = 1,
size: Int = 10,
token: String
) = api.getNewFreeContentOfTheme(
theme = theme,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
authHeader = token
)
fun getPopularContentByCreator(
creatorId: Long,
token: String
) = api.getPopularFreeContentByCreator(
creatorId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
}

View File

@@ -0,0 +1,181 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.free
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.main.ContentCreatorResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.series.GetRecommendSeriesListResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentMainTabFreeViewModel(
private val repository: AudioContentMainTabFreeRepository
) : 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 _contentBannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
val contentBannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
get() = _contentBannerLiveData
private var _introduceCreatorLiveData = MutableLiveData<GetContentCurationResponse>()
val introduceCreatorLiveData: LiveData<GetContentCurationResponse>
get() = _introduceCreatorLiveData
private var _recommendSeriesListLiveData =
MutableLiveData<List<GetRecommendSeriesListResponse>>()
val recommendSeriesListLiveData: LiveData<List<GetRecommendSeriesListResponse>>
get() = _recommendSeriesListLiveData
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _newContentListLiveData
private var _themeListLiveData = MutableLiveData<List<String>>()
val themeListLiveData: LiveData<List<String>>
get() = _themeListLiveData
private val _contentCreatorListLiveData = MutableLiveData<List<ContentCreatorResponse>>()
val contentCreatorListLiveData: LiveData<List<ContentCreatorResponse>>
get() = _contentCreatorListLiveData
private val _playCountRankContentListLiveData =
MutableLiveData<List<GetAudioContentRankingItem>>()
val playCountRankContentListLiveData: LiveData<List<GetAudioContentRankingItem>>
get() = _playCountRankContentListLiveData
private var _curationListLiveData = MutableLiveData<List<GetContentCurationResponse>>()
val curationListLiveData: LiveData<List<GetContentCurationResponse>>
get() = _curationListLiveData
fun fetchData() {
_isLoading.value = true
compositeDisposable.add(
repository.getContentMainFree(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val data = it.data
_contentBannerLiveData.value = data.contentBannerList
if (data.introduceCreator != null) {
_introduceCreatorLiveData.value = data.introduceCreator!!
}
_recommendSeriesListLiveData.value = data.recommendSeriesList
_newContentListLiveData.value = data.newFreeContentList
val themeList = listOf("전체").union(data.themeList).toList()
_themeListLiveData.value = themeList
_contentCreatorListLiveData.value = data.creatorList
_playCountRankContentListLiveData.value =
data.playCountRankContentList
_curationListLiveData.value = data.curationList
} 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 getNewFreeContentOfTheme(theme: String) {
_isLoading.value = true
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(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_isLoading.value = false
}
)
)
}
fun getPopularContentByCreator(creatorId: Long) {
_isLoading.value = true
compositeDisposable.add(
repository.getPopularContentByCreator(
creatorId = creatorId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
_playCountRankContentListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.free
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.ContentCreatorResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.series.GetRecommendSeriesListResponse
@Keep
data class GetContentMainTabLiveFreeResponse(
@SerializedName("contentBannerList")
val contentBannerList: List<GetAudioContentBannerResponse>,
@SerializedName("introduceCreator")
val introduceCreator: GetContentCurationResponse?,
@SerializedName("recommendSeriesList")
val recommendSeriesList: List<GetRecommendSeriesListResponse>,
@SerializedName("themeList")
val themeList: List<String>,
@SerializedName("newFreeContentList")
val newFreeContentList: List<GetAudioContentMainItem>,
@SerializedName("creatorList")
val creatorList: List<ContentCreatorResponse>,
@SerializedName("playCountRankContentList")
val playCountRankContentList: List<GetAudioContentRankingItem>,
@SerializedName("curationList")
val curationList: List<GetContentCurationResponse>
)

View File

@@ -0,0 +1,104 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.free.introduce_creator
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityIntroduceCreatorBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
@OptIn(UnstableApi::class)
class IntroduceCreatorActivity : BaseActivity<ActivityIntroduceCreatorBinding>(
ActivityIntroduceCreatorBinding::inflate
) {
private val viewModel: IntroduceCreatorViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentNewAllAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.getIntroduceCreatorList()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "크리에이터 소개"
binding.toolbar.tvBack.setOnClickListener { finish() }
val spanCount = 3
val spacing = 13.3f.dpToPx().roundToInt()
adapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
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, spanCount)
binding.rvContent.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
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.getIntroduceCreatorList()
}
}
})
binding.rvContent.adapter = adapter
}
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.introduceCreatorListLiveData.observe(this) {
adapter.addItems(it)
}
}
}

View File

@@ -0,0 +1,74 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.free.introduce_creator
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.v2.free.AudioContentMainTabFreeRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class IntroduceCreatorViewModel(
private val repository: AudioContentMainTabFreeRepository
) : 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 _introduceCreatorListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val introduceCreatorListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _introduceCreatorListLiveData
private var isLast = false
private var page = 1
private val size = 10
fun getIntroduceCreatorList() {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getIntroduceCreatorList(
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.isNotEmpty()) {
page += 1
_introduceCreatorListLiveData.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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
}

View File

@@ -0,0 +1,761 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.home
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Color
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
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.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.all.AudioContentRankingAllActivity
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.AudioContentMainActivity
import kr.co.vividnext.sodalive.audio_content.main.v2.AudioContentMainTab
import kr.co.vividnext.sodalive.audio_content.main.v2.ContentRankCreatorAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.PopularContentByCreatorAdapter
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
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.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainTabHomeBinding
import kr.co.vividnext.sodalive.explorer.ExplorerSectionAdapter
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.explorer.profile.series.UserProfileSeriesListAdapter
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.event_banner.EventBannerAdapter
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
import kr.co.vividnext.sodalive.search.SearchActivity
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.notice.NoticeDetailActivity
import kr.co.vividnext.sodalive.settings.notification.MemberRole
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
@UnstableApi
class AudioContentMainTabHomeFragment : BaseFragment<FragmentAudioContentMainTabHomeBinding>(
FragmentAudioContentMainTabHomeBinding::inflate
) {
private val viewModel: AudioContentMainTabHomeViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var rankCreatorAdapter: ExplorerSectionAdapter
private lateinit var rankSeriesAdapter: UserProfileSeriesListAdapter
private lateinit var contentBannerAdapter: AudioContentMainBannerAdapter
private lateinit var rankContentAdapter: AudioContentMainRankingAdapter
private lateinit var rankContentSortAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var contentRankCreatorAdapter: ContentRankCreatorAdapter
private lateinit var popularContentByCreatorAdapter: PopularContentByCreatorAdapter
private val preferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
// 특정 키에 대한 값이 변경될 때 UI 업데이트
if (key == Constants.PREF_USER_ROLE) {
if (
sharedPreferences.getString(
key,
MemberRole.USER.name
) == MemberRole.CREATOR.name
) {
binding.llUploadContent.visibility = View.VISIBLE
binding.llUploadContent.setOnClickListener {
startActivity(
Intent(
requireActivity(),
AudioContentUploadActivity::class.java
)
)
}
} else {
binding.llUploadContent.visibility = View.GONE
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
setupView()
bindData()
viewModel.fetchData()
}
override fun onDestroyView() {
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
super.onDestroyView()
}
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
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
}
if (SharedPreferenceManager.token.isNotBlank()) {
binding.ivCharge.visibility = View.VISIBLE
binding.ivCharge.setOnClickListener {
startActivity(
Intent(
requireContext(),
CanChargeActivity::class.java
)
)
}
} else {
binding.ivCharge.visibility = View.GONE
}
if (SharedPreferenceManager.token.isNotBlank()) {
binding.flSearch.visibility = View.VISIBLE
binding.flSearch.setOnClickListener {
startActivity(
Intent(
requireContext(),
SearchActivity::class.java
)
)
}
} else {
binding.flSearch.visibility = View.GONE
}
setupNotice()
setupContentBanner()
setupCategory()
setupRankCreator()
setupRankSeries()
setupRankContentSortType()
setupRankContent()
setupEventBanner()
setupPopularContentCreator()
setupPopularContentByCreator()
}
private fun setupNotice() {
viewModel.noticeLiveData.observe(viewLifecycleOwner) { notice ->
binding.tvNoticeTitle.text = notice.title
binding.tvDetail.setOnClickListener {
startActivity(
Intent(requireContext(), NoticeDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_NOTICE, notice)
}
)
}
}
}
private fun setupContentBanner() {
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
contentBannerAdapter = 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.SERIES -> {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it.seriesId!!)
}
)
}
AudioContentBannerType.LINK -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
}
}
}
binding
.rvBanner
.layoutParams = layoutParams
binding.rvBanner.apply {
adapter = contentBannerAdapter 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_3bb9f1)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
viewModel.contentBannerLiveData.observe(viewLifecycleOwner) {
if (
SharedPreferenceManager.token.isBlank() ||
(contentBannerAdapter.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)
}
}
}
private fun setupCategory() {
if (SharedPreferenceManager.token.isNotBlank()) {
binding.llCategoryContainer.visibility = View.VISIBLE
} else {
binding.llCategoryContainer.visibility = View.GONE
}
binding.rlCategoryAudioBook.setOnClickListener {
showToast("준비중 입니다.")
}
binding.rlCategoryAudioToon.setOnClickListener {
showToast("준비중 입니다.")
}
binding.rlCategorySeries.setOnClickListener {
startAudioContentMainActivity(AudioContentMainTab.SERIES)
}
binding.rlCategoryContent.setOnClickListener {
startAudioContentMainActivity(AudioContentMainTab.CONTENT)
}
binding.rlCategoryAlarm.setOnClickListener {
startAudioContentMainActivity(AudioContentMainTab.ALARM)
}
binding.rlCategoryAsmr.setOnClickListener {
startAudioContentMainActivity(AudioContentMainTab.ASMR)
}
binding.rlCategoryReplay.setOnClickListener {
startAudioContentMainActivity(AudioContentMainTab.REPLAY)
}
binding.rlCategoryFree.setOnClickListener {
startAudioContentMainActivity(AudioContentMainTab.FREE)
}
}
private fun startAudioContentMainActivity(tab: AudioContentMainTab) {
startActivity(
Intent(requireContext(), AudioContentMainActivity::class.java).apply {
putExtra(
Constants.EXTRA_START_TAB_POSITION,
tab.ordinal
)
}
)
}
private fun setupRankCreator() {
if (SharedPreferenceManager.token.isNotBlank()) {
binding.llCreatorRankDate.visibility = View.VISIBLE
val lp = binding.tvCreatorRankTitle.layoutParams as LinearLayout.LayoutParams
lp.topMargin = 30f.dpToPx().toInt()
binding.tvCreatorRankTitle.layoutParams = lp
} else {
binding.llCreatorRankDate.visibility = View.GONE
val lp = binding.tvCreatorRankTitle.layoutParams as LinearLayout.LayoutParams
lp.topMargin = 0
binding.tvCreatorRankTitle.layoutParams = lp
}
rankCreatorAdapter = ExplorerSectionAdapter(
onClickItem = {
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
},
isVisibleRanking = true
)
binding.rvCreatorRank.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvCreatorRank.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()
}
rankCreatorAdapter.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.rvCreatorRank.adapter = rankCreatorAdapter
viewModel.rankCreatorLiveData.observe(viewLifecycleOwner) {
binding.tvDesc.text = it.desc
binding.tvCreatorRankTitle.text = if (
!it.coloredTitle.isNullOrBlank() &&
!it.color.isNullOrBlank()
) {
val spStr = SpannableString(it.title)
try {
spStr.setSpan(
ForegroundColorSpan(
Color.parseColor("#${it.color}")
),
it.title.indexOf(it.coloredTitle),
it.title.indexOf(it.coloredTitle) + it.coloredTitle.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
spStr
} catch (e: IllegalArgumentException) {
it.title
}
} else {
it.title
}
rankCreatorAdapter.addItems(it.creators)
if (rankCreatorAdapter.itemCount <= 0 && it.creators.isEmpty()) {
binding.llCreatorRank.visibility = View.GONE
binding.rvCreatorRank.visibility = View.GONE
} else {
binding.llCreatorRank.visibility = View.VISIBLE
binding.rvCreatorRank.visibility = View.VISIBLE
}
}
}
private fun setupRankSeries() {
rankSeriesAdapter = UserProfileSeriesListAdapter(
onClickItem = {
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
},
onClickCreator = {
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
},
isVisibleCreator = true
)
val recyclerView = binding.rvRankSeries
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
rankSeriesAdapter.itemCount - 1 -> {
outRect.right = 0
outRect.left = 6.7f.dpToPx().toInt()
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = rankSeriesAdapter
viewModel.rankSeriesLiveData.observe(viewLifecycleOwner) {
rankSeriesAdapter.addItems(it)
binding.llRankSeries.visibility = if (
SharedPreferenceManager.token.isBlank() ||
rankSeriesAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
}
private fun setupRankContentSortType() {
rankContentSortAdapter = AudioContentMainNewContentThemeAdapter {
viewModel.getContentRanking(sort = it)
}
binding.rvRankContentSort.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvRankContentSort.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()
}
rankContentSortAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvRankContentSort.adapter = rankContentSortAdapter
viewModel.rankContentSortListLiveData.observe(viewLifecycleOwner) {
binding.llRankContent.visibility = View.VISIBLE
rankContentSortAdapter.addItems(it)
}
}
private fun setupRankContent() {
binding.ivRankContentAll.setOnClickListener {
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(Intent(requireContext(), AudioContentRankingAllActivity::class.java))
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
}
rankContentAdapter = AudioContentMainRankingAdapter(
width = (screenWidth * 0.66).toInt()
) {
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
}
binding.rvRankContent.layoutManager = GridLayoutManager(
context,
3,
GridLayoutManager.HORIZONTAL,
false
)
binding.rvRankContent.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.rvRankContent.adapter = rankContentAdapter
viewModel.rankContentLiveData.observe(viewLifecycleOwner) {
binding.llRankContent.visibility = View.VISIBLE
rankContentAdapter.addItems(it)
}
}
private fun setupEventBanner() {
val imageSliderLp = binding.eventBannerSlider.layoutParams
imageSliderLp.width = screenWidth
imageSliderLp.height = (screenWidth * 300) / 1000
binding.eventBannerSlider.layoutParams = imageSliderLp
binding.eventBannerSlider.apply {
adapter = EventBannerAdapter(requireContext()) {
if (it.detailImageUrl != null) {
val intent = Intent(requireActivity(), EventDetailActivity::class.java)
intent.putExtra(Constants.EXTRA_EVENT, it)
startActivity(intent)
} else if (!it.link.isNullOrBlank()) {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(it.link)
)
)
}
} as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(800)
}.create()
binding.eventBannerSlider
.setIndicatorView(binding.indicatorEventBanner)
.setIndicatorStyle(IndicatorStyle.ROUND_RECT)
.setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
.setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_3bb9f1)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
viewModel.eventLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty() && SharedPreferenceManager.token.isNotBlank()) {
binding.eventBannerSlider.visibility = View.VISIBLE
binding.eventBannerSlider.refreshData(it)
} else {
binding.eventBannerSlider.visibility = View.GONE
}
}
}
private fun setupPopularContentCreator() {
contentRankCreatorAdapter = ContentRankCreatorAdapter {
binding.llNoItems.visibility = View.VISIBLE
binding.rvRankingSalesCount.visibility = View.GONE
viewModel.getPopularContentByCreator(it)
}
binding.rvRankingCreator.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvRankingCreator.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 = 11f.dpToPx().toInt()
}
contentRankCreatorAdapter.itemCount - 1 -> {
outRect.left = 11f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 11f.dpToPx().toInt()
outRect.right = 11f.dpToPx().toInt()
}
}
}
})
binding.rvRankingCreator.adapter = contentRankCreatorAdapter
viewModel.contentRankCreatorListLiveData.observe(viewLifecycleOwner) {
contentRankCreatorAdapter.addItems(it)
if (contentRankCreatorAdapter.itemCount <= 0 && it.isEmpty()) {
binding.llCreatorContentRanking.visibility = View.GONE
} else {
binding.llCreatorContentRanking.visibility = View.VISIBLE
}
}
}
private fun setupPopularContentByCreator() {
popularContentByCreatorAdapter = PopularContentByCreatorAdapter(
itemWidth = ((screenWidth - 13.3f.dpToPx() * 3) / 2).toInt(),
onClickItem = { contentId ->
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
Intent(requireActivity(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
)
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
},
onClickCreator = { creatorId ->
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
Intent(requireActivity(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, creatorId)
}
)
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
}
)
val recyclerView = binding.rvRankingSalesCount
recyclerView.layoutManager = GridLayoutManager(requireContext(), 2)
recyclerView.addItemDecoration(
GridSpacingItemDecoration(
2,
13.3f.dpToPx().toInt(),
false
)
)
recyclerView.adapter = popularContentByCreatorAdapter
viewModel.salesCountRankContentListLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llNoItems.visibility = View.GONE
recyclerView.visibility = View.VISIBLE
popularContentByCreatorAdapter.addItems(it)
} else {
binding.llNoItems.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
}
}
}
}

View File

@@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.home
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.ContentType
class AudioContentMainTabHomeRepository(private val api: AudioContentApi) {
fun getContentMainHome(token: String) = api.getContentMainHome(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getPopularContentByCreator(
creatorId: Long,
token: String
) = api.getPopularContentByCreator(
creatorId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getContentRanking(sort: String, token: String) = api.getContentMainHomeContentRanking(
sortType = sort,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
}

View File

@@ -0,0 +1,175 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.home
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.main.ContentCreatorResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.GetExplorerSectionResponse
import kr.co.vividnext.sodalive.settings.event.EventItem
import kr.co.vividnext.sodalive.settings.notice.NoticeItem
class AudioContentMainTabHomeViewModel(
private val repository: AudioContentMainTabHomeRepository
) : 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 _noticeLiveData = MutableLiveData<NoticeItem>()
val noticeLiveData: LiveData<NoticeItem>
get() = _noticeLiveData
private var _contentBannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
val contentBannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
get() = _contentBannerLiveData
private val _rankCreatorLiveData = MutableLiveData<GetExplorerSectionResponse>()
val rankCreatorLiveData: LiveData<GetExplorerSectionResponse>
get() = _rankCreatorLiveData
private var _rankSeriesLiveData = MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val rankSeriesLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _rankSeriesLiveData
private var _rankContentSortListLiveData = MutableLiveData<List<String>>()
val rankContentSortListLiveData: LiveData<List<String>>
get() = _rankContentSortListLiveData
private var _rankContentLiveData = MutableLiveData<List<GetAudioContentRankingItem>>()
val rankContentLiveData: LiveData<List<GetAudioContentRankingItem>>
get() = _rankContentLiveData
private val _eventLiveData = MutableLiveData<List<EventItem>>()
val eventLiveData: LiveData<List<EventItem>>
get() = _eventLiveData
private val _contentRankCreatorListLiveData = MutableLiveData<List<ContentCreatorResponse>>()
val contentRankCreatorListLiveData: LiveData<List<ContentCreatorResponse>>
get() = _contentRankCreatorListLiveData
private val _salesCountRankContentListLiveData =
MutableLiveData<List<GetAudioContentRankingItem>>()
val salesCountRankContentListLiveData: LiveData<List<GetAudioContentRankingItem>>
get() = _salesCountRankContentListLiveData
fun fetchData() {
_isLoading.value = true
compositeDisposable.add(
repository.getContentMainHome(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val data = it.data
if (data.latestNotice != null) {
_noticeLiveData.value = data.latestNotice!!
}
_contentBannerLiveData.value = data.bannerList
_rankCreatorLiveData.value = data.rankCreatorList
_rankSeriesLiveData.value = data.rankSeriesList
_rankContentLiveData.value = data.rankContentList
_rankContentSortListLiveData.value = data.rankSortTypeList
_eventLiveData.value = data.eventBannerList.eventList
_contentRankCreatorListLiveData.value = data.contentRankCreatorList
_salesCountRankContentListLiveData.value =
data.salesCountRankContentList
} 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 getContentRanking(sort: String = "매출") {
_isLoading.value = true
compositeDisposable.add(
repository.getContentRanking(
sort = sort,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
_rankContentLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun getPopularContentByCreator(creatorId: Long) {
_isLoading.value = true
compositeDisposable.add(
repository.getPopularContentByCreator(
creatorId = creatorId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
_salesCountRankContentListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@@ -0,0 +1,23 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.home
import androidx.annotation.Keep
import kr.co.vividnext.sodalive.audio_content.main.ContentCreatorResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.explorer.GetExplorerSectionResponse
import kr.co.vividnext.sodalive.settings.event.GetEventResponse
import kr.co.vividnext.sodalive.settings.notice.NoticeItem
@Keep
data class GetContentMainTabHomeResponse(
val latestNotice: NoticeItem?,
val bannerList: List<GetAudioContentBannerResponse>,
val rankCreatorList: GetExplorerSectionResponse,
val rankSeriesList: List<GetSeriesListResponse.SeriesListItem>,
val rankSortTypeList: List<String>,
val rankContentList: List<GetAudioContentRankingItem>,
val eventBannerList: GetEventResponse,
val contentRankCreatorList: List<ContentCreatorResponse>,
val salesCountRankContentList: List<GetAudioContentRankingItem>
)

View File

@@ -0,0 +1,448 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.replay
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
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.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainContentAdapter
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.AudioContentMainContentCurationAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.ContentRankCreatorAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.PopularContentByCreatorAdapter
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainTabReplayBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.event_banner.EventBannerAdapter
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
@OptIn(UnstableApi::class)
class AudioContentMainTabReplayFragment : BaseFragment<FragmentAudioContentMainTabReplayBinding>(
FragmentAudioContentMainTabReplayBinding::inflate
) {
private val viewModel: AudioContentMainTabReplayViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var contentBannerAdapter: AudioContentMainBannerAdapter
private lateinit var newContentAdapter: AudioContentMainContentAdapter
private lateinit var curationAdapter: AudioContentMainContentCurationAdapter
private lateinit var contentRankCreatorAdapter: ContentRankCreatorAdapter
private lateinit var popularContentByCreatorAdapter: PopularContentByCreatorAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
bindData()
viewModel.fetchData()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupContentBanner()
setupNewContent()
setupPopularContentCreator()
setupPopularContentByCreator()
setupEventBanner()
setupCuration()
}
private fun setupContentBanner() {
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
contentBannerAdapter = 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.SERIES -> {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it.seriesId!!)
}
)
}
AudioContentBannerType.LINK -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
}
}
}
binding
.rvBanner
.layoutParams = layoutParams
binding.rvBanner.apply {
adapter = contentBannerAdapter 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_3bb9f1)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
viewModel.contentBannerLiveData.observe(viewLifecycleOwner) {
if (contentBannerAdapter.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)
}
}
}
private fun setupNewContent() {
binding.ivNewContentAll.setOnClickListener {
startActivity(
Intent(requireContext(), ReplayNewContentAllActivity::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
viewModel.newContentListLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llNewContent.visibility = View.VISIBLE
newContentAdapter.addItems(it)
} else {
binding.llNewContent.visibility = View.GONE
}
}
}
private fun setupPopularContentCreator() {
contentRankCreatorAdapter = ContentRankCreatorAdapter {
binding.llNoItems.visibility = View.VISIBLE
binding.rvRankingSalesCount.visibility = View.GONE
viewModel.getPopularContentByCreator(it)
}
binding.rvRankingCreator.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvRankingCreator.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 = 11f.dpToPx().toInt()
}
contentRankCreatorAdapter.itemCount - 1 -> {
outRect.left = 11f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 11f.dpToPx().toInt()
outRect.right = 11f.dpToPx().toInt()
}
}
}
})
binding.rvRankingCreator.adapter = contentRankCreatorAdapter
viewModel.contentCreatorListLiveData.observe(viewLifecycleOwner) {
contentRankCreatorAdapter.addItems(it)
if (contentRankCreatorAdapter.itemCount <= 0 && it.isEmpty()) {
binding.llCreatorContentRanking.visibility = View.GONE
} else {
binding.llCreatorContentRanking.visibility = View.VISIBLE
}
}
}
private fun setupPopularContentByCreator() {
popularContentByCreatorAdapter = PopularContentByCreatorAdapter(
itemWidth = ((screenWidth - 13.3f.dpToPx() * 3) / 2).toInt(),
onClickItem = { contentId ->
startActivity(
Intent(requireActivity(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
)
},
onClickCreator = { creatorId ->
startActivity(
Intent(requireActivity(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, creatorId)
}
)
}
)
val recyclerView = binding.rvRankingSalesCount
recyclerView.layoutManager = GridLayoutManager(requireContext(), 2)
recyclerView.addItemDecoration(
GridSpacingItemDecoration(
2,
13.3f.dpToPx().toInt(),
false
)
)
recyclerView.adapter = popularContentByCreatorAdapter
viewModel.salesCountRankContentListLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llNoItems.visibility = View.GONE
recyclerView.visibility = View.VISIBLE
popularContentByCreatorAdapter.addItems(it)
} else {
binding.llNoItems.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
}
}
}
private fun setupEventBanner() {
val imageSliderLp = binding.eventBannerSlider.layoutParams
imageSliderLp.width = screenWidth
imageSliderLp.height = (screenWidth * 300) / 1000
binding.eventBannerSlider.layoutParams = imageSliderLp
binding.eventBannerSlider.apply {
adapter = EventBannerAdapter(requireContext()) {
if (it.detailImageUrl != null) {
val intent = Intent(requireActivity(), EventDetailActivity::class.java)
intent.putExtra(Constants.EXTRA_EVENT, it)
startActivity(intent)
} else if (!it.link.isNullOrBlank()) {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(it.link)
)
)
}
} as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(800)
}.create()
binding.eventBannerSlider
.setIndicatorView(binding.indicatorEventBanner)
.setIndicatorStyle(IndicatorStyle.ROUND_RECT)
.setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
.setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_3bb9f1)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
viewModel.eventLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.eventBannerSlider.visibility = View.VISIBLE
binding.indicatorEventBanner.visibility = View.VISIBLE
binding.eventBannerSlider.refreshData(it)
} else {
binding.eventBannerSlider.visibility = View.GONE
binding.indicatorEventBanner.visibility = View.GONE
}
}
}
private fun setupCuration() {
curationAdapter = AudioContentMainContentCurationAdapter(
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.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 = 30f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
curationAdapter.itemCount - 1 -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 30f.dpToPx().toInt()
}
else -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
}
}
})
binding.rvCuration.adapter = curationAdapter
viewModel.curationListLiveData.observe(viewLifecycleOwner) {
curationAdapter.addItems(it)
binding.rvCuration.visibility = if (curationAdapter.itemCount <= 0 && it.isEmpty()) {
View.GONE
} else {
View.VISIBLE
}
}
}
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
}
}

View File

@@ -0,0 +1,23 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.replay
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.ContentType
class AudioContentMainTabReplayRepository(private val api: AudioContentApi) {
fun getContentMainReplay(token: String) = api.getContentMainReplay(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getPopularContentByCreator(
creatorId: Long,
token: String
) = api.getPopularReplayContentByCreator(
creatorId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
}

View File

@@ -0,0 +1,124 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.replay
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.main.ContentCreatorResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.event.EventItem
class AudioContentMainTabReplayViewModel(
private val repository: AudioContentMainTabReplayRepository
) : 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 _contentBannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
val contentBannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
get() = _contentBannerLiveData
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _newContentListLiveData
private val _contentCreatorListLiveData = MutableLiveData<List<ContentCreatorResponse>>()
val contentCreatorListLiveData: LiveData<List<ContentCreatorResponse>>
get() = _contentCreatorListLiveData
private val _salesCountRankContentListLiveData =
MutableLiveData<List<GetAudioContentRankingItem>>()
val salesCountRankContentListLiveData: LiveData<List<GetAudioContentRankingItem>>
get() = _salesCountRankContentListLiveData
private val _eventLiveData = MutableLiveData<List<EventItem>>()
val eventLiveData: LiveData<List<EventItem>>
get() = _eventLiveData
private var _curationListLiveData = MutableLiveData<List<GetContentCurationResponse>>()
val curationListLiveData: LiveData<List<GetContentCurationResponse>>
get() = _curationListLiveData
fun fetchData() {
_isLoading.value = true
compositeDisposable.add(
repository.getContentMainReplay(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val data = it.data
_contentBannerLiveData.value = data.contentBannerList
_newContentListLiveData.value = data.newLiveReplayContentList
_contentCreatorListLiveData.value = data.creatorList
_salesCountRankContentListLiveData.value =
data.salesCountRankContentList
_eventLiveData.value = data.eventBannerList.eventList
_curationListLiveData.value = data.curationList
} 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 getPopularContentByCreator(creatorId: Long) {
_isLoading.value = true
compositeDisposable.add(
repository.getPopularContentByCreator(
creatorId = creatorId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
_salesCountRankContentListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

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