Compare commits

..

367 Commits
v1.1.0 ... main

Author SHA1 Message Date
klaus 5a37ba8be0 라이브방
- 룰렛설정, 룰렛, 후원 버튼 터치시 키보드가 내려가도록 설정
2024-10-30 15:21:59 +09:00
klaus 37d47efe2c 라이브 후원 - 비밀후원 체크박스
- 체크박스가 선택된 이미지로 변경되지 않던 버그 수정
2024-10-30 12:02:31 +09:00
klaus f6c5be24d8 콘텐츠, 커뮤니티 댓글 입력창
- 길게 입력하면 줄바꿈 되어 화면 내에서 입력한 글이 모두 보이도록 수정
2024-10-29 14:44:19 +09:00
klaus 0c8241fba7 콘텐츠 대여 가격
- 소장가격의 60%에서 70% 표기로 변경
2024-10-29 14:33:40 +09:00
klaus dc2cda58b2 라이브
- 우측 하단 옵션 버튼 순서 변경
2024-10-29 13:54:36 +09:00
klaus 936074081c 라이브 후원
- 비밀후원을 체크하면 힌트 메시지에 '비밀' 추가
- 후원메시지 최대 길이 50 -> 200 변경
2024-10-29 00:30:34 +09:00
klaus a556378ffe 라이브방 - 라이브 후원랭킹 리스트
- 방장은 일반후원 / 비밀후원 캔을 나눠서 보이도록 수정
2024-10-28 23:49:30 +09:00
klaus 960dda8d40 콘텐츠 상세 - 콘텐츠 설명글
- textSize: 14
- color: 909090
- font: gmarket_sans_medium
2024-10-28 13:54:13 +09:00
klaus a401a2e13a versionCode 120, versionName 1.22.2 2024-10-28 13:47:19 +09:00
klaus f51f7ef412 라이브방
- 하트 알림 바(채팅) 배경, 닉네임, 글자색 변경
- 하트 알림 바 위치 수정 : 채팅 -> 공지 밑
2024-10-27 23:36:28 +09:00
klaus 04151168ca 라이브방
- 채팅창 너비 화면 가득 채움
2024-10-27 21:51:13 +09:00
klaus 99a93001bc 알람 권한 설정
- 사용 하지 않는 권한을 런타임 권한으로 물어 문제가 생기는 버그 수정
2024-10-27 18:28:20 +09:00
klaus e6339bb4c2 개수 제한 콘텐츠
- 내 콘텐츠는 sold out 되더라도 재생이 가능하도록 수정
2024-10-24 23:52:50 +09:00
klaus 7209f972d2 라이브 방 - 채팅 EditText
- inputType에서 textMultiLine를 제거하여 imeOptions의 actionSend설정이 적용되도록 수정
2024-10-24 01:45:47 +09:00
klaus 0714918338 라이브 방 - 하트 후원 채팅
- 채팅 배경색 변경
2024-10-23 01:39:03 +09:00
klaus 94d581a4f3 라이브 방 - 하트 후원 애니메이션
- 룰렛의 활성화/비활성화시 키보드를 숨기고 하트 후원 애니메이션 시작 위치 재계산
2024-10-22 22:16:13 +09:00
klaus 577e864b6a 라이브 방 - 하트 후원 애니메이션
- 스피커/리스너 변경시 키보드를 숨기고 하트 후원 애니메이션 시작 위치 재계산
2024-10-22 21:43:56 +09:00
klaus 96a3ef44f6 라이브 방
- 하트 후원시 채팅으로 알림
2024-10-17 15:01:05 +09:00
klaus 3a33153361 라이브 방
- 하트 후원 안내 팝업 추가
2024-10-16 19:08:39 +09:00
klaus ad0c18dceb 라이브 방
- 하트 총 개수 조회 기능 추가
2024-10-16 18:31:14 +09:00
klaus e964679154 라이브 방
- 하트 후원 API 연결
- 하트 후원 성공시 하트 애니메이션 호출
2024-10-16 18:06:38 +09:00
klaus 6c9ace146d 라이브 방
- 하트 애니메이션 추가
2024-10-16 15:54:08 +09:00
klaus c7409e4dec 라이브 방
- 채팅창 너비 축소
- 오른쪽 하단 옵션 버튼 baseline이 채팅창 baseline과 동일하게 설정
- 좋아요(누르면 1캔 후원) 버튼 추가
- 좋아요 개수 UI - 후원 캔 왼쪽에 추가
2024-10-16 02:25:55 +09:00
klaus a4ff89cec0 사용하지 않는 아이콘 제거 2024-10-15 23:54:52 +09:00
klaus b489f46910 푸시 알림 아이콘 - 앱 로고 아이콘으로 변경 2024-10-15 23:53:31 +09:00
klaus 4a167a00bd 권한 수정 2024-10-15 19:02:10 +09:00
klaus 2aca7620e7 권한 수정 2024-10-14 17:51:43 +09:00
klaus 1f7f3bfdb1 큐레이션 콘텐츠
- 남성향이면 여성 크리에이터, 여성향이면 남성 크리에이터 작품만 조회되도록 수정
2024-10-14 02:21:39 +09:00
klaus 90ff8ceb72 콘텐츠 메인 - 추천시리즈, 모닝콜, 숏플, 라이브 다시보기
- 남성향이면 여성 크리에이터, 여성향이면 남성 크리에이터 작품만 조회되도록 수정
2024-10-14 01:34:35 +09:00
klaus e8b69cc6b9 가이드 이미지 변경 2024-10-13 22:04:35 +09:00
klaus 9143e74a72 소다로 살다 -> 보이스 모닝콜 등록 변경 2024-10-13 21:55:51 +09:00
klaus 7bb6e2ae45 스플래시 이미지
- 글자 크기 변경
2024-10-13 21:21:02 +09:00
klaus 6f4f500aec 소다라이브 -> 보이스온 2024-10-12 01:17:48 +09:00
klaus d328bdb4d1 스플래시 변경 2024-10-12 01:03:26 +09:00
klaus 7cb3d19e85 콘텐츠 전체 보기
- 콘텐츠 제목 영역과 가격 표시 영역이 겹치지 않도록 수정
2024-10-11 19:40:25 +09:00
klaus 7c320b7f23 콘텐츠, 라이브 메인
- 보이스 모닝콜 메뉴 추가
- 라이브 다시듣기 메뉴 라이브 메인으로 이동
2024-10-11 14:11:56 +09:00
klaus f2cca1e14b 설정
- 19금 콘텐츠 보기 설정 UI depth 추가
- 콘텐츠 보기 설정으로 제목 변경
2024-10-10 15:42:19 +09:00
klaus f3553c3c59 라이브, 콘텐츠 메인 - 새로운 콘텐츠, 큐레이션
- 민감한 콘텐츠(19금) 설정 추가
2024-10-10 13:36:33 +09:00
klaus 5daddc5fef 설정
- 19금 콘텐츠 보기 설정 UI 추가
2024-10-10 13:10:40 +09:00
klaus 8cee9fb019 라이브 메뉴 설정 페이지 추가 2024-10-08 16:13:30 +09:00
klaus 38e4122570 크리에이터 채널
- 메뉴 설정 버튼 추가
2024-10-07 23:41:59 +09:00
klaus 3c178dbb96 룰렛 설정 완료 메시지
- 채널에서 새 룰렛을 저장할 때 성공메시지 수정
2024-10-04 13:58:40 +09:00
klaus 4537a95d2d 불필요한 파일 삭제 2024-10-03 00:50:50 +09:00
klaus fd4dfc2dff 시리즈 소개
- 연재 요일 표시에서 마지막에 있는 요일 제거
2024-10-02 18:06:04 +09:00
klaus 2d9ec631aa 스플래시 2024년 10월 2024-10-02 15:23:27 +09:00
klaus b59172d608 라이브 방 - 시그니처 이미지
- 로딩이 완료된 시점부터 이미지 표시 시간을 계산하여 최대한 오차가 없도록 조정
2024-09-30 22:03:49 +09:00
klaus 790e42035f 크리에이터 채널
- 룰렛 설정 메뉴 추가
2024-09-30 21:10:35 +09:00
klaus 7782fe389e 라이브 수정
- 테두리 색 변경
2024-09-25 14:52:56 +09:00
klaus 84b64d3283 라이프 참여자 리스트
- 스피커 표시 최대 5명
2024-09-24 19:39:32 +09:00
klaus 66a83a118f 콘텐츠 메인
- 배너 indicator 색상 변경
2024-09-24 14:08:30 +09:00
klaus 3494ff0491 콘텐츠 상세
- 시간표시 슬라이더 색상변경
2024-09-24 14:01:50 +09:00
klaus d51fc88813 콘텐츠 업로드
- 미리듣기 최소 시간 15초로 변경
2024-09-24 13:47:14 +09:00
klaus 1930014498 라이브 비밀후원
- 방장인 경우 비밀후원도 후원 총액에 반영되도록 수정
2024-09-24 12:14:47 +09:00
klaus 4189ee4e54 라이브
- 큰 음소거 이미지 변경
2024-09-20 16:12:13 +09:00
klaus a191e295da 라이브
- 최대 스피커 수 방장 포함 6명으로 변경
2024-09-20 16:10:48 +09:00
klaus 0709c68653 크리에이터 채널
- 공유버튼 변경
2024-09-20 15:34:00 +09:00
klaus 084c306159 시리즈 상세 - 크리에이터 팔로우와 알림설정
- 팔로잉 상태에서 알림 켜기/끄기 상태 추가
2024-09-20 12:11:33 +09:00
klaus 2ed77a3332 콘텐츠 상세 - 크리에이터 팔로우와 알림설정
- 팔로잉 상태에서 알림 켜기/끄기 상태 추가
2024-09-20 11:44:13 +09:00
klaus 5b2c5a6e2f 팔로잉/팔로워 리스트 - 팔로우와 알림설정
- 팔로잉 상태에서 알림 켜기/끄기 상태 추가
2024-09-20 01:57:27 +09:00
klaus 5063ce815d 크리에이터 채널 - 팔로우와 알림설정
- 팔로잉 상태에서 알림 켜기/끄기 상태 추가
2024-09-13 16:11:39 +09:00
klaus 78323103fd 라이브 정보 수정
- 연령 제한 설정 추가
2024-09-11 23:26:45 +09:00
klaus 772005910c 시리즈 콘텐츠 리스트
- 정렬(최신순, 등록순) 추가
2024-09-10 16:27:32 +09:00
klaus 1f36d8ef25 커뮤니티 댓글, 팬토크, 콘텐츠 댓글
- 프로필 이미지 터치시 차단, 신고가 가능한 유저 프로필 표시
2024-09-07 01:28:50 +09:00
klaus 1b8d65c3ea 라이브 방 - 유저 차단 팝업
- 리스너가 차단할 때 팝업 문구 수정
2024-09-05 20:01:16 +09:00
klaus 07df9ced75 PG 결제 - 카카오페이 결제
- 구매자 정보 추가
2024-09-05 16:07:27 +09:00
klaus 9dfd73b090 라이브 방
- 차단한 유저의 채팅이 보이지 않도록 수정
2024-09-04 22:29:55 +09:00
klaus 0ebb34a2df 차단 리스트 페이지 추가 2024-09-04 12:31:32 +09:00
klaus 6869cd62ea 팔로잉 리스트
- 팔로우 한 채널이 없는 경우 '팔로우 중인 채널이 없습니다.' 표시
2024-09-02 18:01:18 +09:00
klaus b7b73bb409 마이 페이지
- 팔로잉 리스트 버튼 추가
2024-09-02 17:47:28 +09:00
klaus 7bf3728cf9 커뮤니티 댓글
- 비밀 댓글 체크박스 보이지 않도록 수정
2024-09-02 14:32:12 +09:00
klaus cc01312ae5 콘텐츠 댓글
- 일반 댓글의 닉네임이 잘리는 버그 수정
2024-08-30 19:00:35 +09:00
klaus cf94615e71 라이브 방 비밀후원, 콘텐츠 비밀댓글
- 체크박스 네모로 변경
2024-08-30 18:17:06 +09:00
klaus 51cb36b2e6 라이브 방
- 후원현황과 후원하기 버튼 순서 변경
2024-08-30 15:07:35 +09:00
klaus 345df7a7b7 콘텐츠 댓글 리스트
- 비밀댓글은 닉네임 옆에 '비밀댓글' 마크 추가
2024-08-30 15:03:46 +09:00
klaus 4961727237 콘텐츠 댓글 리스트
- 댓글이 없을 때 유료 콘텐츠를 구매한 사람이 비밀댓글을 등록할 수 있는 기능 추가
2024-08-30 14:42:20 +09:00
klaus 6640130ef9 콘텐츠 상세
- 댓글이 없을 때 유료 콘텐츠를 구매한 사람이 비밀댓글을 등록할 수 있는 기능 추가
2024-08-30 13:11:56 +09:00
klaus e38778d9e7 캔 결제수단 추가
- 카카오페이
2024-08-27 19:33:58 +09:00
klaus 63e9b705a9 스플래시 2024/09 2024-08-27 00:06:41 +09:00
klaus 38864ca666 포그라운드 서비스 시작시 foregroundServiceTypes 추가 2024-08-26 15:24:45 +09:00
klaus adab4aee48 라이브 후원
- 비밀 후원 기능 추가
2024-08-24 00:37:53 +09:00
klaus 3bad5c5e55 콘텐츠 메인
- 우측 최상단에 콘텐츠 보관함, 알람 아이콘 추가
- 배너와 추천 시리즈 순서 변경
2024-08-21 21:49:42 +09:00
klaus 7607c10bdc @Keep 어노테이션을 추가하여 난독화에서 제외되도록 수정 2024-08-21 20:22:39 +09:00
klaus 4349f2bd3a targetSdk 34로 변경
커스텀 액션에서 패키지를 명시
2024-08-21 12:44:51 +09:00
klaus 62abd3c900 라이브
- 후원 메시지, 룰렛 결과 모든 유저에게 보이도록 수정
2024-08-14 18:34:39 +09:00
klaus 63193c82a1 versionCode 96, versionName 1.15.1 2024-08-12 17:22:45 +09:00
klaus 6431577bf1 알람
- 날짜 선택 추가
2024-08-12 16:58:23 +09:00
klaus b56a0d58bf 알람
- 볼륨 설정 추가
2024-08-09 15:22:17 +09:00
klaus 00774739ed 팬토크
- 응원글 등록/수정/삭제시 전체 데이터를 지우고 다시 조회하도록 수정
2024-08-08 02:19:26 +09:00
klaus b98cc8ed79 크리에이터 커뮤니티 오디오 녹음
- 파일명 수정
2024-08-08 02:02:21 +09:00
klaus 81faf3f7ee 크리에이터 커뮤니티 게시글 오디오 재생
- 크리에이터 커뮤니티 게시글 오디오 재생시 백그라운드에서 재생 중인 오디오 콘텐츠 일시정지
2024-08-06 12:14:15 +09:00
klaus 14c72e8259 크리에이터 커뮤니티 게시글 전체리스트
- 오디오 콘텐츠 재생기능 추가
2024-08-06 00:11:35 +09:00
klaus 94d719a814 크리에이터 커뮤니티 게시글 등록- 오디오 녹음 다이얼로그
- x버튼을 눌렀을 때 올리지 않을 파일을 삭제하는 로직 추가
- 드래그 해서 화면이 꺼지지 않도록 수정
2024-08-05 21:31:03 +09:00
klaus 66fecf1509 크리에이터 커뮤니티 게시글 등록- 오디오 녹음
- 녹음 시간 : centiseconds 까지 표시
- 음질 조정 - sampleRate: 48k, bitrate: 256k
2024-08-05 17:54:18 +09:00
klaus e6e3df701d 크리에이터 커뮤니티 게시글 등록
- 오디오 녹음 기능 추가
2024-08-02 18:03:56 +09:00
klaus 76aaaddb5a 음성메시지
- 속닥 -> 메시지
- 다시 녹음 버튼 글자색 #3bb9f1
2024-08-02 17:05:28 +09:00
klaus fa23263b19 온보딩 이미지 변경 2024-07-31 23:32:10 +09:00
klaus 2ba2014f82 알람 설정
- 처음 설정시 알람 id값을 받아오지 않아 알람이 울리지 않던 버그 id값을 받아오도록 수정
2024-07-31 22:08:31 +09:00
klaus ee6090c103 알람 권한 요청 코드 추가 2024-07-31 19:59:48 +09:00
klaus 063158e9e0 회사정보 고객센터 이용시간 추가 2024-07-31 18:30:20 +09:00
klaus a1ac80c180 versionCode 90, versionName 1.14.0 2024-07-31 12:10:47 +09:00
klaus 42a8c43ff5 앱 가이드 이미지 변경 2024-07-30 22:46:00 +09:00
klaus 1fcd3cc309 앱 아이콘 변경 2024-07-30 22:16:56 +09:00
klaus 66cdb621ca 스플래시 변경 2024-07-30 21:34:15 +09:00
klaus a618903f0f 알람
일회성 알람의 경우 알람이 울리면 날짜를 다음날로 변경하고 isEnable을 false로 변경한다.
2024-07-29 12:20:58 +09:00
klaus 4fc39f10b1 알람
setExactAndAllowWhileIdle -> setAlarmClock 로 변경
2024-07-29 11:42:52 +09:00
klaus b393ddcd74 label 수정
알람 -> 소다로 살다
2024-07-26 16:05:23 +09:00
klaus 7aa80154fa label 수정
알람 -> 소다로 살다
2024-07-26 15:03:09 +09:00
klaus 262f42c194 PG 심사를 위해
- 소장 기간을 PG심사용 계정이면 (이용기간 1년)으로 나오도록 수정
2024-07-26 14:45:01 +09:00
klaus 18d16a2211 sdk 31이상에서 PendingIntent.FLAG_IMMUTABLE FLAG 추가 2024-07-26 01:43:00 +09:00
klaus 7c91795fb5 알람
- 추가 슬롯 구매하기 기능 추가
2024-07-26 00:52:34 +09:00
klaus 856e13d37b 알람
- 알람음 -> 콘텐츠로 변경
2024-07-25 18:21:33 +09:00
klaus 2223919a98 알람
- 소장한 콘텐츠만 알람으로 지정 가능하다는 안내문구 추가
2024-07-25 18:20:24 +09:00
klaus 7b4063420e 알람
- 최대 3개까지 등록되도록 수정
2024-07-25 16:24:43 +09:00
klaus 839ff7463e 알람 기능 추가 2024-07-25 16:10:38 +09:00
klaus 7587b8bc25 PG 결제 - 휴대폰
- 헥토파이낸셜로 변경
2024-07-19 15:06:38 +09:00
klaus 756675e622 versionCode 82 2024-07-05 21:18:47 +09:00
klaus 2f059cc783 룰렛 합계 검증
- Float 값이라 정확히 100.0이 나오지 않으므로 totalPercentage가 99.9보다 크고 100.1보다 작으면 통과되도록 조건 수정
2024-07-02 21:32:23 +09:00
klaus 573d5a89f9 회원가입
- 개인정보 처리방침 터치시 이용약관이 나오는 버그 수정
2024-07-02 18:39:30 +09:00
klaus 40472bdced PG 수정 - payment method
- 디지털 카드 -> 카드
- 디지털 계좌이체 -> 계좌이체
2024-07-02 14:49:20 +09:00
klaus ac7124a7e6 PG 수정
- 헥토 검증 API 추가
2024-06-29 18:59:51 +09:00
klaus 888674b776 PG 수정
- 휴대폰 결제: 웰컴페이먼츠
- 나머지 : 헥토파이낸스(세틀뱅크)
2024-06-29 18:33:30 +09:00
klaus c9f6088754 라이브 방
- 공유하기 추가
2024-06-27 19:28:49 +09:00
klaus 687aada611 콘텐츠 업로드
- 한정판 업로드 추가
2024-06-18 16:13:56 +09:00
klaus 20a1e8f1d7 versionCode "80"
versionName "1.12.3"
2024-06-05 19:16:04 +09:00
klaus 357d6c3406 크리에이터 커뮤니티
- 커뮤니티 글이 보이지 않던 버그 수정
2024-06-05 19:13:12 +09:00
klaus 40ff959289 콘텐츠 리스트
- 소장중 / 대여중 / Sold Out 표시
2024-06-04 19:56:46 +09:00
klaus 22825d1520 회사정보 주소 수정 2024-05-30 13:26:40 +09:00
klaus 827a8258ec 크리에이터 커뮤니티 게시물
- 구매하지 않은 유료 게시물도 내용은 보이도록 수정
2024-05-30 13:24:40 +09:00
klaus 8505be882c 스플래시 06월 2024-05-28 16:23:28 +09:00
klaus d0eb4103ba 크리에이터 커뮤니티
- 유료 & 구매하지 않은 게시글만 이미지가 Blur 처리 되도록 수정
2024-05-28 12:23:54 +09:00
klaus ba83027dc9 회사정보 대표 이메일 추가 2024-05-25 00:09:40 +09:00
klaus 7b3fa0b224 콘텐츠 메인 탭
- 하단에 회사정보 추가
2024-05-24 23:38:16 +09:00
klaus eb7f83433c 마이페이지 탭
- PG심사용 계정의 경우 캔 표시 영역 뷰 제거
2024-05-24 00:19:24 +09:00
klaus 9d5ca7c36d 커뮤니티 유료 게시글 조회, 구매 기능 추가 2024-05-23 23:13:25 +09:00
klaus a9511dcb51 커뮤니티 게시글 등록
- 유료 게시글 등록은 이미지가 있어야 등록이 가능하도록 수정
2024-05-23 01:27:29 +09:00
klaus 5d42e2b16e 커뮤니티 게시글 등록
- 유료 게시글 등록을 위해 가격 설정 추가
2024-05-23 00:05:35 +09:00
klaus 6fac6ecd4d 확률을 입력하지 않아서 crash 나는 버그 수정 2024-05-22 19:33:15 +09:00
klaus b876695adc 회사정보 변경 2024-05-21 01:25:17 +09:00
klaus b220b8bce4 콘텐츠 상세, 콘텐츠 구매
- pg 테스트 계정의 경우 캔이 아닌 원으로 표시되도록 하고 콘텐츠 구매시 바로 결제 후 구매 되도록 수정
2024-05-20 15:37:46 +09:00
klaus ba4d707d45 9970ff -> 3bb9f1 2024-05-17 15:35:26 +09:00
klaus 2920d2a7ae 9970ff -> 3bb9f1 2024-05-17 15:28:59 +09:00
klaus b8abef6aeb 9970ff -> 3bb9f1 2024-05-17 15:11:41 +09:00
klaus 75fc34cbe3 9970ff -> 3bb9f1 2024-05-17 15:09:47 +09:00
klaus 206fc398c6 라이브, 회원 태그
- 9970ff -> 3bb9f1 색상 변경
2024-05-16 14:23:07 +09:00
klaus b95b77bcb9 룰렛 설정
- 옵션 확률 총합 추가
2024-05-16 12:41:20 +09:00
klaus 62c269cac2 룰렛 설정
- 옵션 추가시 확률이 빈칸으로 표시되어 NumberFormat Exception이 나오던 버그 예외처리
2024-05-16 12:11:05 +09:00
klaus 4b3474ff42 라이브 방 생성
- 크리에이터 입장 가능 설정 추가
2024-05-14 17:06:12 +09:00
klaus d6e9b929e9 라이브 방
- 음소거 버튼 위치 아래로 이동
2024-05-11 04:00:26 +09:00
klaus 13efce42a0 룰렛 설정
- 룰렛 1, 2, 3 버튼 bg, text 색상 변경
2024-05-11 03:55:58 +09:00
klaus 85ccc18485 룰렛 변경
- 확률 수동 설정
- 여러개의 룰렛이 켜져있을 때 선택하여 돌리기
- 후원 히스토리에 룰렛 히스토리 추가
2024-05-10 17:55:20 +09:00
klaus 9df08cdf24 콘텐츠 메인
- 새로운 콘텐츠 아래 새로고침 버튼 제거
2024-05-08 12:17:28 +09:00
klaus 7892eed443 라이브
- 라이브가 없을 때 문구 수정
- 라이브가 없을 떄 문구 자간 및 폰트 사이즈 수정
2024-05-08 12:15:13 +09:00
klaus eca68c06c3 라이브 메인 - 라이브 없을 때 문구
- 🙀마이페이지에서 본인인증을 하거나 라이브를 예약하고 참여해보세요.
2024-05-07 19:27:34 +09:00
klaus 3c82ff1c4e 라이브 메인
- 당겨서 새로고침 제거
- 새로고침 버튼 추가
2024-05-07 19:02:58 +09:00
klaus 8c6aff1623 콘텐츠 메인 - 추천 시리즈, 새로운 콘텐츠
- 새로고침 버튼 추가
2024-05-07 18:58:15 +09:00
klaus 254a1e3381 콘텐츠 메인
- 추천 시리즈 UI 추가
2024-05-07 16:42:14 +09:00
klaus dff4c833f1 . 2024-05-04 02:47:03 +09:00
klaus 5fa3a591d4 인 앱 결제
- 서버 호출 후 충전이 완료되면 충전완료 페이지로 이동하도록 수정
2024-05-03 19:47:19 +09:00
klaus 845578a1dd 시리즈 아이템
- 커버이미지 DIM 제거
2024-05-03 13:41:30 +09:00
klaus 84ac72b391 탐색
- 크리에이터가 없으면 섹션제거
2024-05-03 13:36:41 +09:00
klaus db364d9bf7 라이브
- 시그니처 ON/OFF 버튼 추가
- 공유하기 버튼 제거
2024-05-02 15:17:00 +09:00
klaus 3fe01f8def 2024년 5월 인트로 적용 2024-05-01 22:44:16 +09:00
klaus 02c815077e 시그니처 후원
- 시그니처 별로 설정된 시간 만큼 GIF가 재생되도록 기능 추가
2024-05-01 22:14:58 +09:00
klaus 108eb759ec 시리즈 전체보기
아이템 세로 간격 수정
2024-05-01 00:39:17 +09:00
klaus fef49a0d6a 시리즈 전체보기
아이템 사이즈 수정
2024-04-30 22:33:37 +09:00
klaus 87241fa8bd 시그니처 후원
- 위치 가운데로 수정
2024-04-30 19:55:47 +09:00
klaus 29d5192fff 시리즈 콘텐츠
- 대여중/소장중/가격 뱃지가 동시에 표시되는 버그 수정
2024-04-30 19:28:16 +09:00
klaus c86e55719e 인 앱 결제
- 사용하지 않는 함수 제거
2024-04-30 17:18:19 +09:00
klaus 839a8a780c 시리즈 상세 작품소개
- 0원 -> 무료로 변경
2024-04-30 14:57:32 +09:00
klaus 346334a0ba 시리즈 상세 작품소개
- 구분선 색 변경
2024-04-30 13:32:10 +09:00
klaus 7cd4d180c2 시리즈 상세 콘텐츠
- 제목이 가격영역을 침범하는 버그 수정
2024-04-29 11:47:24 +09:00
klaus 320ef4fbc7 시리즈 상세
- 19금과 전체연령가가 반대로 표시되던 버그 수정
2024-04-29 11:41:13 +09:00
klaus b4623141f3 크리에이터 채널
- 시리즈 데이터가 파싱되지 않던 버그 수정
2024-04-27 04:23:38 +09:00
klaus 30d3ed14d7 시리즈 콘텐츠 리스트 아이템
- 출시 날짜 제거
- 재생 시간 제목 윗쪽으로 이동
2024-04-27 03:14:09 +09:00
klaus ae617d5154 크리에이터 채널 시리즈
- 이미지 사이즈
가로 102 -> 116.7, 세로 144 -> 165 변경
2024-04-27 03:10:51 +09:00
klaus 9dd3c568d8 시리즈 상세
- 스크롤시 타이틀 윗쪽에 배경이 살짝 보이던 버그 수정
2024-04-27 02:54:39 +09:00
klaus f31fc7691e 시리즈 상세
- 내용 스크롤 시 뒤로가기 버튼도 같이 스크롤 되도록 수정
2024-04-27 02:01:53 +09:00
klaus 27df922383 시리즈 상세 작품소개
- 키워드 세로 간격 수정
- 상세정보 글자색 흰색으로 변경
2024-04-27 01:57:54 +09:00
klaus d5956c024d 시리즈 상세
- 팔로잉 액션 추가
2024-04-27 01:25:02 +09:00
klaus 29aca74651 시리즈 상세
- 전체회차 보기 사이즈 16으로 변경
- 태그 글자 크기 12로 변경
2024-04-27 01:08:22 +09:00
klaus f41790b302 시리즈 콘텐츠 전체보기
- 제목 - 전체회차 듣기 로 변경
2024-04-27 00:47:44 +09:00
klaus 0b999a874c 시리즈 콘텐츠 전체보기 페이지 추가 2024-04-27 00:34:42 +09:00
klaus 2778638dc9 시리즈 상세보기
- 콘텐츠 링크 추가
2024-04-26 23:27:26 +09:00
klaus cc5fe445fc 시리즈 상세보기 페이지 추가 2024-04-26 23:17:44 +09:00
klaus c310f9c57e 시리즈 전체보기 페이지 추가 2024-04-25 22:05:58 +09:00
klaus cd607425a0 크리에이터 채널
- 시리즈 section 추가
2024-04-25 17:14:19 +09:00
klaus a5df8a1110 크리에이터 채널
- 활동요약표 선 색깔 button색으로 수정
2024-04-22 15:05:16 +09:00
klaus 2bd30aa346 versionCode 57, versionName 1.9.11
인 앱 결제 로직 수정
- 결제 완료 후 서버에서 데이터 처리 후 로컬에서 다시 소비처리를 하도록 수정
2024-04-22 14:40:16 +09:00
klaus a6ce994fd0 versionCode 49, versionName 1.9.2 2024-04-12 19:57:44 +09:00
klaus 7bffd1c3c7 후원하기 팝업
- 캔 충전하기 페이지로 이동하는 버튼 직관적으로 보이도록 화살표에서 충전 글자로 변경
2024-04-12 14:30:30 +09:00
klaus dfd92d6db6 음소거 버튼 위치 - top
크리에이터 팔로우 버튼 margin_top = 5.3
시그니처 후원 움짤 위치 - 우측 중간으로 이동
2024-04-12 14:09:47 +09:00
klaus 5529872bd5 본인인증을 하지 않아도 PG결제가 보이도록 수정 2024-04-05 11:46:18 +09:00
klaus 364a530956 크리에이터 커뮤니티 대댓글
- 부모 댓글과 동일한 글자 색상으로 변경
2024-04-02 15:36:49 +09:00
klaus 0556d5a067 오픈 예정 콘텐츠 상세
- 댓글 창, 좋아요, 공유, 후원 버튼 숨김
2024-04-02 14:47:56 +09:00
klaus be46893555 캔 충전 페이지
- PG, IAP 순서로 탭 변경
2024-04-02 00:33:12 +09:00
klaus f7f789892d okhttp connect, read, write timeout 시간 60초 설정 2024-04-01 16:38:58 +09:00
klaus de0d327168 인트로
- 4월 인트로로 변경
2024-04-01 15:48:06 +09:00
klaus e52075a692 라이브
- 팔로우/팔로잉 버튼 변경
2024-03-28 02:01:24 +09:00
klaus a29c50eae3 콘텐츠 상세
- 한정판이 매진된 경우 Player 가운데 Sold Out 표시
2024-03-28 01:35:21 +09:00
klaus 8a2a497fcf 콘텐츠 상세
- 한정판이 매진된 경우 구매버튼 비활성화 및 '해당 콘텐츠가 매진되었습니다.' 문구 표시
2024-03-27 18:59:28 +09:00
klaus 6786988e63 콘텐츠 상세
- 한정판이 매진된 경우 구매버튼 비활성화 및 '해당 콘텐츠가 매진되었습니다.' 문구 표시
2024-03-27 16:59:29 +09:00
klaus 2dd5e2d96c 콘텐츠 상세
- 한정판인 경우 대여/소장 선택 다이얼로그를 생략하고 바로 구매확인 다이얼로그가 나오도록 수정
2024-03-27 16:40:15 +09:00
klaus 03320d9cec 콘텐츠 상세
- 한정판 UI 추가
2024-03-27 00:04:28 +09:00
klaus 2101cdeb86 인 앱 결제 추가 2024-03-25 17:40:17 +09:00
klaus f0a8ca5823 Toast show 로직 수정 2024-03-23 06:05:08 +09:00
klaus daed389264 인 앱 결제 로직 수정
versionName 1.8.16, versionCode 41
2024-03-23 05:37:25 +09:00
klaus 6a558ad25c versionName 1.8.7, versionCode 32 2024-03-22 12:07:56 +09:00
klaus 6e3a4e1125 캔 충전페이지
- 인 앱 결제 페이지 추가
2024-03-21 04:14:52 +09:00
klaus 79cb4b995a 캔 충전페이지
- 인 앱 결제 추가를 위해 단일 액티비티에서 탭 구성으로 변경
2024-03-20 01:24:39 +09:00
klaus 84b9dd0841 versionCode 27 versionName "1.8.2" 2024-03-15 18:05:52 +09:00
klaus 4004dcd99e 라이브 메인 지금 라이브 중
- 이미지 크기 가로 116 -> 128, 세로 164 -> 179
2024-03-15 00:49:11 +09:00
klaus ebbe7e8917 라이브 메인 지금 라이브 중, 라이브 전체 보기
- 이미지 크기 수정, 잠금 아이콘 사이즈 40 -> 60
2024-03-14 23:51:37 +09:00
klaus 196d5c6cfd 라이브 메인 - 지금 라이브 중
- 입장 가능한 잔여 인원 표시 제거
2024-03-14 23:20:00 +09:00
klaus f4626a7cd5 라이브 - 호스트는 리스너로 변경 버튼이 보이지 않도록 수정 2024-03-14 22:57:04 +09:00
klaus 4ed97d4600 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 19:13:21 +09:00
klaus c2cf05ef64 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 18:46:22 +09:00
klaus 0e131f6661 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 18:27:25 +09:00
klaus d091c47a0c 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 18:01:45 +09:00
klaus 6bd5d26882 지금 라이브 중 전체보기 - UI 표시 방식 수정
- 1줄에 1개 보이던 리스트 방식에서 1줄에 3개씩 표시하는 그리드 방식으로 변경
2024-03-14 17:37:05 +09:00
klaus e02ea116ff 지금 라이브 중 - 참여 가능 인원 표시 2024-03-14 17:22:37 +09:00
klaus 012d1d94d5 지금 라이브 중 - 라이브 아이템 유/무료 표시 방식 수정
- 무료 배경색 : #111111
- 유료 배경색 : #DD4500
- 유료 표시 - white 캔과 함께 100 과 같이 가격으로 변경
2024-03-14 16:50:57 +09:00
klaus 39e49b08d9 본인이 스피커일 때 리스너로 변경하는 버튼 추가 2024-03-14 14:48:43 +09:00
klaus 2a84d2dc41 시그니처 재생 길이 수정
- 3초에서 7초로 변경
2024-03-14 14:41:21 +09:00
klaus 66c9b38e04 versionCode 26 versionName "1.8.1" 2024-03-11 12:24:44 +09:00
klaus 8f35f7a573 라이브 정보 수정
- 메뉴 설정을 하지 않고 다른 라이브 정보만 수정 했을 경우 앱이 종료 되는 버그 수정
(selectedMenu가 초기화 되지 않아도 라이브 정보를 수정할 수 있도록 수정)
2024-03-11 12:16:49 +09:00
klaus c653563512 라이브 만들기
- 메뉴 등록 edittext 스크롤 적용
2024-03-08 23:04:54 +09:00
klaus a75821ed06 스플래시 화면 변경 2024-03-08 21:04:51 +09:00
klaus ca6416c697 라이브 정보 변경 다이얼로그
- 공지, 메뉴 입력 창 스크롤 적용
2024-03-08 04:55:49 +09:00
klaus f7b3caf320 스플래시 화면 변경 2024-03-08 04:49:34 +09:00
klaus 62d839b69b 시그니처 후원 이미지 표시
- gif만 표시되던 것 모든 이미지로 변경
2024-03-08 04:47:47 +09:00
klaus a9c3ea953d 시그니처 후원 이미지 표시
- 이미지 URL을 배열에 저장 후 순서대로 표시하도록 수정
2024-03-08 02:46:25 +09:00
klaus 5be9720fcc 시그니처 후원 이미지 표시
- 하단 마진 20 -> 65로 변경
2024-03-08 01:37:55 +09:00
klaus ec096b5831 후원
- 시그니처 후원 적용
2024-03-08 00:51:43 +09:00
klaus 51c5e5f32c 에러가 아닌데 에러로 표시하는 부분 warning으로 변경 2024-03-07 06:33:10 +09:00
klaus b6d7e0b0e9 불필요한 로그 제거 2024-03-07 06:32:22 +09:00
klaus 49209c4c4a 라이브 정보 수정
- 메뉴판 수정 기능 추가
2024-03-07 06:31:25 +09:00
klaus 6b466ac7d7 라이브 메인 지금 라이브 중
- 가격 유/무료 뱃지 배경색 3bb9f1(버튼색)으로 변경
2024-03-07 02:36:05 +09:00
klaus d6182f9e03 라이브 메인 추천 채널
- 라이브 중 표시 컬러 9970ff -> 3bb9f1로 변경
2024-03-07 02:33:28 +09:00
klaus 2e24a298ff 라이브 만들기
- 메뉴판 설정 추가
2024-03-07 02:29:38 +09:00
klaus d7d43bc7be 라이브
- 메뉴판 UI 추가
2024-03-06 17:02:37 +09:00
klaus 2d0c4ea738 룰렛 설정 개수에 따라 룰렛 프리셋 버튼 활성화/비활성화 2024-02-28 03:56:51 +09:00
klaus af4e802259 룰렛 설정 문구 수정 2024-02-28 03:39:42 +09:00
klaus ad9e97161c 커뮤니티 댓글, 콘텐츠 댓글, 팬토크
- 여러줄 인 경우 줄간격 수정
2024-02-28 02:25:44 +09:00
klaus 1712f509dc gaid 업데이트 추가 2024-02-26 21:56:10 +09:00
klaus 78dd3b2785 커뮤니티 댓글, 콘텐츠 댓글, 팬토크
- 글자 크기, 색상 수정
2024-02-24 23:30:56 +09:00
klaus f7f4638526 룰렛 프리셋 설정
- 변동 사항이 있을 경우에만 업데이트 하도록 수정
2024-02-23 18:39:42 +09:00
klaus 66690a6f89 룰렛 프리셋 설정 성공 메시지
- 현재 선택된 룰렛 프리셋의 활성화/비활성화에 따라 성공 메시지가 수정되도록 수정
2024-02-23 17:57:49 +09:00
klaus 4e56eb9bde 룰렛 프리셋 적용 2024-02-23 14:07:52 +09:00
klaus e3349e41a1 versionCode 23 versionName "1.6.1" 2024-02-16 14:10:34 +09:00
klaus 239ccb9018 튜토리얼 이미지 변경 2024-02-16 14:09:49 +09:00
klaus 6ff17e1ba6 콘텐츠 그리드 리스트
- 아이템 상하 간격 16으로 변경
2024-02-15 00:58:52 +09:00
klaus 1c88a90024 튜토리얼 이미지 순서 변경 2024-02-15 00:55:41 +09:00
klaus dd54e5b97a 튜토리얼 수정 2024-02-14 23:59:28 +09:00
klaus f4180eec14 콘텐츠 메인 숏플, 라이브 다시 듣기 버튼 수정
- 부가 설명 제거
- 버튼 배경 이미지로 변경
- 글자 크기 14.7 -> 16.7로 변경
- 아이콘 큰 사이즈로 변경
2024-02-14 23:39:53 +09:00
klaus b81576bdaf 스플래시 화면 이미지 변경 2024-02-14 23:30:40 +09:00
klaus bf43926d7d 라이브 방
- 후원 시 '20캔을' 색을 바뀌던 것을 '20캔'까지만 색이 바뀌도록 수정
2024-02-14 23:12:15 +09:00
klaus 1ec0d8540a 콘텐츠 리스트 아이템 - 사이즈, 간격 수정 2024-02-14 18:19:47 +09:00
klaus 91a2da9032 versionCode 22 versionName "1.6.0" 2024-02-14 16:49:36 +09:00
klaus a2cbeb87d5 콘텐츠 메인
- 숏플, 라이브 다시 듣기 추가
2024-02-14 02:10:30 +09:00
klaus a88f8c3316 큐레이션
- 아이템 2개에서 3개로 변경
2024-02-13 14:17:57 +09:00
klaus e51af38b75 큐레이션
- 아이템 2개에서 3개로 변경
2024-02-13 02:21:28 +09:00
klaus 6a92f955ca versionCode 22 versionName "1.5.2" 2024-02-13 00:35:44 +09:00
klaus a7ef5a8147 라이브 상세
- 크리에이터는 예약자(참여자) 표시
2024-02-13 00:34:34 +09:00
klaus a14263bf27 versionCode 21 versionName "1.5.1" 2024-02-07 20:38:31 +09:00
klaus 489b09a54e 콘텐츠 전체보기 카테고리
- 아이템 간격 13.3 -> 8 로 수정
2024-02-07 18:39:58 +09:00
klaus 51b57a0b1d 콘텐츠 전체보기
- 카테고리 추가
2024-02-07 07:28:14 +09:00
klaus eafb922ae9 설정페이지
- 회사정보 추가
2024-02-05 22:06:44 +09:00
klaus 6cfbdd7acd 콘텐츠 상세
- 미리듣기가 없을 떄 문구 추가
2024-01-29 16:23:38 +09:00
klaus f8fd06706b 크리에이터 콘텐츠 리스트
- 고정 콘텐츠 핀 추가
2024-01-29 00:48:45 +09:00
klaus c23ef771be 콘텐츠 상세
- 콘텐츠 고정/해제 기능 추가
2024-01-29 00:33:50 +09:00
klaus 0bbb1e070c 콘텐츠 상세
- 미리듣기 없는 콘텐츠는 재생 버튼이 보이지 않도록 수정
2024-01-26 13:31:51 +09:00
klaus fba11ae4b9 콘텐츠 업로드
- 미리듣기 여부 선택 버튼 추가
2024-01-26 02:43:55 +09:00
klaus 6fbb98ca7b 유료방 입장 팝업
- UI 수정
- 알림 문구 수정
2024-01-22 16:09:53 +09:00
klaus b33c8ffd6d 유료방 입장 팝업
- 라이브 시작 시각 -> 시작 시각 으로 변경
2024-01-22 03:19:12 +09:00
klaus 14b3bfbae7 유료방 입장 팝업
- 1시간 이상 지난 후 팝업 내용 변경
- 라이브 시작 시각, 현재 시각 표시
2024-01-21 17:34:07 +09:00
klaus 7386c93d73 커뮤니티 글 - 줄 간격 0 -> 8 로 변경 2024-01-19 16:16:47 +09:00
klaus 4af8d7dce1 라이브 - 배경이미지 가운데 정렬 2024-01-18 18:46:59 +09:00
klaus 08d2ccdab4 라이브 - 배경이미지 상단정렬 2024-01-18 18:27:06 +09:00
klaus 0a8abeefbe 앱이 켜져 있을 때 딥링크가 동작하지 않던 버그 수정 2024-01-17 03:53:41 +09:00
klaus f90e089f8a 프로필 이미지 변경
- 이미지 변경시 로컬에 저장된 프로필 이미지 URL도 변경 하도록 수정
2024-01-17 03:14:04 +09:00
klaus 1eb905fe30 라이브
- 상단 총 받은 캔 사이즈 10->14로 변경
2024-01-16 22:44:14 +09:00
klaus fcc55288b1 라이브
- 오른쪽 하단 버튼 패딩 수정
2024-01-16 22:37:03 +09:00
klaus 97c5dc4363 라이브
- 9970ff 색상 3bb9f1로 변경
2024-01-16 22:18:05 +09:00
klaus 4223f0bc5d 라이브
- 후원, 입장, 룰렛 결과 채팅 배경색 변경
2024-01-16 21:34:48 +09:00
klaus 7ada4515aa 라이브 - 공지 활성화/비활성화 버튼 색 변경 2024-01-16 19:38:46 +09:00
klaus 8456f2b30b 라이브 - 19 배경 변경 2024-01-16 18:56:47 +09:00
klaus c20802f89c 라이브 - 공지 UI 수정 2024-01-16 18:20:03 +09:00
klaus 804f993b25 라이브 - 아고라 로컬 사용자의 음성 활동 감지를 비활성화 하여 말하는 유저 표시를 좀 더 자연스럽게 수정 2024-01-16 17:24:11 +09:00
klaus 869b83804d 크리에이터가 말할 때 크리에이터 배경 표시 2024-01-16 15:59:34 +09:00
klaus 9d97feb184 라이브 공지 버튼 수정 2024-01-16 15:53:51 +09:00
klaus 42a69609a1 라이브 배경 이미지 사이즈 비율
400:564로 변경
2024-01-16 15:10:15 +09:00
klaus 3a541f71a6 라이브 상단 UI 변경 2024-01-16 05:31:23 +09:00
klaus d75e4af348 댓글 쓰기 텍스트 필드 border 색상 변경 2024-01-15 23:38:30 +09:00
klaus 2dac54b3ec 크리에이터 커뮤니티
- 링크 적용
2024-01-15 23:36:02 +09:00
klaus 82cf1658cb 라이브 방
- 말하는 사람 배경 변경
- 배경 On/Off 색상 변경
- 참여자 숫자 색상 변경, 폰트 Bold
2024-01-15 23:11:08 +09:00
klaus 127e1f6673 본인이 스피커 인 경우 말하는 사람 배경이 잘 보이지 않는 버그 수정 2024-01-15 21:05:30 +09:00
klaus 22f1e4024e versionName 1.4.0, versionCode 18 2024-01-11 16:04:51 +09:00
klaus b0bfe2ac06 콘텐츠 상세
- 오픈 예정 날짜 데이터가 있더라도 콘텐츠를 올린 크리에이터의 경우 재생버튼이 동작하도록 수정
2024-01-11 00:27:45 +09:00
klaus d67bb8be50 크리에이터 채널, 콘텐츠 상세
- 오픈예정 추가
2024-01-10 01:57:04 +09:00
klaus eeac702cd7 다이얼로그
- 취소 버튼 글자색 변경
2024-01-09 19:46:37 +09:00
klaus c2216ab054 라이브 예약완료
- 홈으로 이동, 예약이 완료되었습니다 string 글자색 변경
2024-01-08 20:23:36 +09:00
klaus 45e5494653 콘텐츠 업로드
- 예약 업로드를 위해 날짜/시간 선택 추가
2024-01-08 15:10:30 +09:00
klaus 5f7ed22711 라이브 만들기 액티비티
- 요즘라이브 컬러에서 소다라이브 컬러로 변경
2024-01-05 22:15:07 +09:00
klaus 49a823895d 콘텐츠 업로드 액티비티
- 요즘라이브 컬러에서 소다라이브 컬러로 변경
2024-01-05 17:38:01 +09:00
klaus 92324e3409 커뮤니티
- 이미지가 잘려서 보이는 현상 수정
2024-01-04 15:41:05 +09:00
klaus 13057af98a 쿠폰번호 입력 필터 수정
AS-IS : 영대문자와 숫자만 입력되도록 필터적용

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

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

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

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>

View File

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

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>

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 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>

329
.idea/other.xml Normal file
View File

@ -0,0 +1,329 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="direct_access_persist.xml">
<option name="deviceSelectionList">
<list>
<PersistentDeviceSelectionData>
<option name="api" value="27" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="F01L" />
<option name="id" value="F01L" />
<option name="manufacturer" value="FUJITSU" />
<option name="name" value="F-01L" />
<option name="screenDensity" value="360" />
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="SH-01L" />
<option name="id" value="SH-01L" />
<option name="manufacturer" value="SHARP" />
<option name="name" value="AQUOS sense2 SH-01L" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="Lenovo" />
<option name="codename" value="TB370FU" />
<option name="id" value="TB370FU" />
<option name="manufacturer" value="Lenovo" />
<option name="name" value="Tab P12" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="a51" />
<option name="id" value="a51" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A51" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="akita" />
<option name="id" value="akita" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="b0q" />
<option name="id" value="b0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S22 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="32" />
<option name="brand" value="google" />
<option name="codename" value="bluejay" />
<option name="id" value="bluejay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="id" value="comet" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="crownqlteue" />
<option name="id" value="crownqlteue" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Note9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm3q" />
<option name="id" value="dm3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S23 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e1q" />
<option name="id" value="e1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix_camera" />
<option name="id" value="felix_camera" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold (Camera-enabled)" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8uwifi" />
<option name="id" value="gts8uwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8 Ultra" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="husky" />
<option name="id" value="husky" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8 Pro" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="motorola" />
<option name="codename" value="java" />
<option name="id" value="java" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="G20" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="lynx" />
<option name="id" value="lynx" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
<option name="codename" value="oriole" />
<option name="id" value="oriole" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="panther" />
<option name="id" value="panther" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q5q" />
<option name="id" value="q5q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold5" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1812" />
<option name="screenY" value="2176" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q6q" />
<option name="id" value="q6q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-F956B" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1856" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="r11" />
<option name="id" value="r11" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Watch" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="redfin" />
<option name="id" value="redfin" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 5" />
<option name="screenDensity" value="440" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="shiba" />
<option name="id" value="shiba" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="tangorpro" />
<option name="id" value="tangorpro" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Tablet" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
</project>

View File

@ -14,7 +14,7 @@ plugins {
android {
namespace 'kr.co.vividnext.sodalive'
compileSdk 33
compileSdk 34
viewBinding {
enabled true
@ -24,11 +24,6 @@ android {
dataBinding true
}
lintOptions {
checkDependencies true
checkReleaseBuilds false
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
@ -39,9 +34,9 @@ android {
defaultConfig {
applicationId "kr.co.vividnext.sodalive"
minSdk 23
targetSdk 33
versionCode 10
versionName "1.1.0"
targetSdk 34
versionCode 122
versionName "1.22.3"
}
buildTypes {
@ -51,17 +46,20 @@ 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"'
}
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"'
}
@ -73,6 +71,10 @@ android {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
lint {
checkDependencies true
checkReleaseBuilds false
}
}
dependencies {
@ -135,10 +137,10 @@ dependencies {
implementation 'com.google.firebase:firebase-config-ktx'
// 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
@ -150,6 +152,13 @@ dependencies {
implementation "com.michalsvec:single-row-calednar:1.0.0"
// PointClick Maven Remote Repo
implementation 'kr.co.pointclick.sdk.offerwall:pointclick-sdk-offerwall:1.0.17'
// 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"
}

View File

@ -222,9 +222,10 @@
-keep class kr.co.bootpay.core.** { *; }
-keep class kr.co.pointclick.sdk.offerwall.core.consts.** {*;}
-keep interface kr.co.pointclick.sdk.offerwall.core.consts.** {*;}
-keep class kr.co.pointclick.sdk.offerwall.core.models.** {*;}
-keep interface kr.co.pointclick.sdk.offerwall.core.models.** {*;}
-keep class kr.co.pointclick.sdk.offerwall.core.PointClickAd {*;}
-keep class kr.co.pointclick.sdk.offerwall.core.events.PackageReceiver {*;}
-keep class retrofit2.** { *; }
-keep class com.google.gson.** { *; }
-keep class sun.misc.** { *; }
# @Keep 애노테이션이 붙은 클래스, 메서드, 필드를 보호
-keep @androidx.annotation.Keep class * { *; }

View File

@ -2,17 +2,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<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"
@ -37,6 +38,13 @@
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" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
@ -83,8 +91,12 @@
<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" />
@ -94,8 +106,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" />
@ -106,6 +120,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" />
@ -124,6 +139,27 @@
<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=".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"
@ -134,9 +170,13 @@
<service
android:name=".common.SodaLiveService"
android:foregroundServiceType="microphone|mediaPlayback"
android:stopWithTask="false" />
<service android:name=".audio_content.AudioContentPlayService" />
<service
android:name=".audio_content.AudioContentPlayService"
android:foregroundServiceType="mediaPlayback"
android:stopWithTask="false" />
<!-- [START firebase_service] -->
<service
@ -148,6 +188,21 @@
</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"

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

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

View File

@ -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

@ -2,6 +2,7 @@ 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
@ -10,14 +11,17 @@ import kr.co.vividnext.sodalive.audio_content.detail.GetAudioContentDetailRespon
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeResponse
import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationRequest
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentCurationResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRanking
import kr.co.vividnext.sodalive.audio_content.main.GetNewContentUploadCreator
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListResponse
import kr.co.vividnext.sodalive.audio_content.order.OrderRequest
import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse
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
@ -35,6 +39,7 @@ interface AudioContentApi {
@GET("/audio-content")
fun getAudioContentList(
@Query("creator-id") id: Long,
@Query("category-id") categoryId: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort-type") sort: AudioContentViewModel.Sort,
@ -46,6 +51,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(
@ -125,20 +141,19 @@ 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("theme") theme: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
@ -159,6 +174,8 @@ interface AudioContentApi {
@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,
@ -182,4 +199,40 @@ interface AudioContentApi {
@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/new-content-upload-creator")
fun getNewContentUploadCreatorList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetNewContentUploadCreator>>>
@GET("/audio-content/main/banner-list")
fun getMainBannerList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentBannerResponse>>>
@GET("/audio-content/main/order-list")
fun getMainOrderList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@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>>
}

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
@ -205,6 +210,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PAUSE
@ -220,6 +226,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PLAY
@ -254,6 +261,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_LOADING,
true
@ -317,6 +325,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI,
true
@ -341,6 +350,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 +377,7 @@ class AudioContentPlayService :
mediaPlayer.setOnCompletionListener(this)
mediaPlayer.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
@ -378,6 +389,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PLAY
@ -403,6 +415,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
false
@ -468,7 +481,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 +511,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?) {

View File

@ -1,9 +1,12 @@
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.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.ContentType
import kr.co.vividnext.sodalive.user.CreatorFollowRequestRequest
import kr.co.vividnext.sodalive.user.UserApi
import okhttp3.MultipartBody
@ -12,7 +15,8 @@ import java.util.TimeZone
class AudioContentRepository(
private val api: AudioContentApi,
private val userApi: UserApi
private val userApi: UserApi,
private val categoryApi: CategoryApi
) {
fun getAudioContentListByCurationId(
curationId: Long,
@ -22,6 +26,8 @@ class AudioContentRepository(
token: String
) = api.getAudioContentListByCurationId(
id = curationId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
sort = sort,
@ -30,12 +36,14 @@ class AudioContentRepository(
fun getAudioContentList(
id: Long,
categoryId: Long,
page: Int,
size: Int,
sort: AudioContentViewModel.Sort,
token: String
) = api.getAudioContentList(
id = id,
categoryId = categoryId,
page = page - 1,
size = size,
sort = sort,
@ -129,10 +137,10 @@ 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
)
@ -143,6 +151,8 @@ class AudioContentRepository(
token: String
) = api.getNewContentAllOfTheme(
theme = theme,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
authHeader = token
@ -177,4 +187,49 @@ class AudioContentRepository(
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 getNewContentUploadCreatorList(
token: String
) = api.getNewContentUploadCreatorList(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
)
}

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

@ -9,9 +9,10 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentNewAllBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
@ -89,8 +90,10 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
}
private fun setupNewContent() {
val spanCount = 3
val spacing = 40
newContentAdapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - 40f.dpToPx().toInt()) / 2,
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
@ -107,44 +110,8 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
}
)
binding.rvContent.layoutManager = GridLayoutManager(this, 2)
binding.rvContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
if (position % 2 == 0) {
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
} else {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
}
when (position) {
0, 1 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
newContentAdapter.itemCount - 1, newContentAdapter.itemCount - 2 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvContent.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) {

View File

@ -1,17 +1,27 @@
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 coil.transform.RoundedCornersTransformation
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,
@ -20,23 +30,27 @@ class AudioContentNewAllAdapter(
) : 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) {
binding.ivAudioContentCoverImage.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(2.7f.dpToPx()))
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
}
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)
@ -47,6 +61,16 @@ class AudioContentNewAllAdapter(
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) }
}
@ -58,6 +82,7 @@ class AudioContentNewAllAdapter(
parent: ViewGroup,
viewType: Int
) = ViewHolder(
parent.context,
ItemAudioContentNewAllBinding.inflate(
LayoutInflater.from(parent.context),
parent,

View File

@ -9,7 +9,7 @@ import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog

View File

@ -1,8 +1,10 @@
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.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentAllByThemeBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
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)
}
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,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

@ -9,6 +9,7 @@ import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import com.orhanobut.logger.Logger
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentBinding
@ -19,7 +20,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 +32,24 @@ class AudioContentCommentAdapter(
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentCommentListItem) {
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 +58,27 @@ class AudioContentCommentAdapter(
binding.tvDonationCan.text = can.moneyFormat()
binding.llDonationCan.setBackgroundResource(
when {
can >= 100000 -> {
can >= 10000 -> {
R.drawable.bg_round_corner_10_7_973a3a
}
can >= 50000 -> {
can >= 5000 -> {
R.drawable.bg_round_corner_10_7_d85e37
}
can >= 10000 -> {
can >= 1000 -> {
R.drawable.bg_round_corner_10_7_d38c38
}
can >= 5000 -> {
can >= 500 -> {
R.drawable.bg_round_corner_10_7_59548f
}
can >= 1000 -> {
can >= 100 -> {
R.drawable.bg_round_corner_10_7_4d6aa4
}
can >= 500 -> {
can >= 50 -> {
R.drawable.bg_round_corner_10_7_2d7390
}

View File

@ -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
}

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

@ -17,6 +17,7 @@ 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
@ -54,8 +55,10 @@ class AudioContentCurationActivity : BaseActivity<ActivityAudioContentCurationBi
binding.toolbar.tvBack.text = title
binding.toolbar.tvBack.setOnClickListener { finish() }
val spanCount = 3
val spacing = 40
adapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - 40f.dpToPx().toInt()) / 2,
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
@ -72,44 +75,8 @@ class AudioContentCurationActivity : BaseActivity<ActivityAudioContentCurationBi
}
)
binding.rvCuration.layoutManager = GridLayoutManager(this, 2)
binding.rvCuration.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
if (position % 2 == 0) {
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
} else {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
}
when (position) {
0, 1 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1, adapter.itemCount - 2 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvCuration.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) {

View File

@ -1,8 +1,10 @@
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

@ -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,12 +10,17 @@ 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.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
@ -29,20 +36,25 @@ import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderConfirmDial
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderFragment
import kr.co.vividnext.sodalive.audio_content.order.OrderType
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.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
class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBinding>(
ActivityAudioContentDetailBinding::inflate
@ -58,14 +70,16 @@ 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 audioContent: GetAudioContentDetailResponse
private lateinit var orderType: OrderType
private lateinit var imm: InputMethodManager
@SuppressLint("SetTextI18n")
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
@ -85,11 +99,21 @@ 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() }
}
@ -97,7 +121,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
override fun onResume() {
super.onResume()
val intentFilter = IntentFilter(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
registerReceiver(audioContentReceiver, intentFilter)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(audioContentReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(audioContentReceiver, intentFilter)
}
if (refresh) {
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
@ -115,10 +143,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.tvBack.setOnClickListener { finish() }
binding.ivClosePreviewAlert.setOnClickListener { viewModel.toggleShowPreviewAlert() }
binding.ivMenu.setOnClickListener {
showOptionMenu(
this,
binding.ivMenu,
)
showOptionMenu()
}
creatorOtherContentAdapter = OtherContentAdapter {
@ -240,16 +265,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()) {
@ -259,7 +290,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
}
dialog.show(screenWidth)
dialog.show(screenWidth - 26.7f.dpToPx().toInt())
}
}
@ -269,50 +300,46 @@ 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() {
@ -387,18 +414,6 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
setupCommentArea(it)
setupCreatorOtherContentListArea(it.creatorOtherContentList)
setupSameThemeOtherContentList(it.sameThemeOtherContentList)
isAlertPreview = it.creator.creatorId != SharedPreferenceManager.userId &&
!it.existOrdered &&
it.price > 0
binding.ivPlayOrPause.setImageResource(
if (isAlertPreview) {
R.drawable.btn_audio_content_preview_play
} else {
R.drawable.btn_audio_content_play
}
)
}
viewModel.isContentPlayLoopLiveData.observe(this) {
@ -445,7 +460,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
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}"
@ -459,8 +478,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
@ -471,9 +495,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 {}
@ -484,10 +521,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,
@ -496,26 +534,84 @@ 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()
binding.tvStrPurchaseOrRental.text = if (response.isOnlyRental) {
" 대여하기"
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, isOnlyRental = response.isOnlyRental)
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 {
"캔으로"
}
binding.tvStrPurchaseOrRental.text = if (response.isOnlyRental) {
" 대여하기"
} else {
" 구매하기"
}
binding.llPurchase.setOnClickListener {
if (
response.totalContentCount != null &&
response.remainingContentCount != null
) {
showOrderConfirmDialog(
audioContent = response,
isOnlyRental = false,
OrderType.KEEP
)
} else {
showOrderDialog(
audioContent = response,
isOnlyRental = response.isOnlyRental
)
}
}
}
} else {
binding.llPurchase.visibility = View.GONE
binding.tvPurchaseSoldOut.visibility = View.GONE
}
}
@ -529,29 +625,72 @@ 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.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(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.ivPlayOrPause.setImageResource(
if (!isAlertPreview) {
R.drawable.btn_audio_content_play
} else {
R.drawable.btn_audio_content_preview_play
}
)
} else if (response.releaseDate == null) {
binding.tvPreviewNo.visibility = View.VISIBLE
}
binding.tvTotalDuration.text = " / ${response.duration}"
}
@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
@ -585,45 +724,81 @@ 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.shareAudioContent(
audioContentId = audioContentId,
contentImage = response.coverImageUrl,
contentTitle = "${response.title} - ${response.creator.nickname}"
) {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, it)
val shareIntent = Intent.createChooser(intent, "오디오콘텐츠 공유")
startActivity(shareIntent)
}
}
} 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}"
}
} else {
binding.rlLimitedEdition.visibility = View.GONE
}
}
@ -669,21 +844,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 {
@ -735,18 +939,52 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.rlPreviewAlert.visibility = View.GONE
viewModel.order(
contentId = audioContent.contentId,
orderType = orderType
) {
val intent = Intent(applicationContext, CanChargeActivity::class.java)
intent.putExtra(Constants.EXTRA_GO_TO_PREV_PAGE, true)
startActivity(intent)
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?) {

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

@ -25,9 +25,11 @@ 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,6 +104,41 @@ class AudioContentDetailViewModel(
)
}
fun follow(contentId: Long, creatorId: Long, follow: Boolean = true, notify: Boolean = true) {
isLoading.value = true
compositeDisposable.add(
userRepository.creatorFollow(
creatorId = creatorId,
follow,
notify,
token = "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 registerNotification(contentId: Long, creatorId: Long) {
isLoading.value = true
compositeDisposable.add(
@ -254,7 +291,7 @@ class AudioContentDetailViewModel(
)
}
fun registerComment(audioContentId: Long, comment: String) {
fun registerComment(audioContentId: Long, comment: String, isSecret: Boolean) {
if (!isLoading.value!!) {
isLoading.value = true
}
@ -263,6 +300,7 @@ class AudioContentDetailViewModel(
commentRepository.registerComment(
contentId = audioContentId,
comment = comment,
isSecret = isSecret,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
@ -349,7 +387,7 @@ class AudioContentDetailViewModel(
}
socialMetaTagParameters {
title = contentTitle
description = "지금 소다라이브에서 이 콘텐츠 감상하기"
description = "지금 보이스온에서 이 콘텐츠 감상하기"
imageUrl = contentImage.toUri()
}
}.addOnSuccessListener {
@ -481,4 +519,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,11 @@
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.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,6 +16,11 @@ 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("isOnlyRental") val isOnlyRental: Boolean,
@ -29,18 +36,24 @@ data class GetAudioContentDetailResponse(
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentList") val commentList: List<GetAudioContentCommentListItem>,
@SerializedName("commentCount") val commentCount: Int,
@SerializedName("isPin") val isPin: Boolean,
@SerializedName("isAvailablePin") val isAvailablePin: Boolean,
@SerializedName("creator") val creator: AudioContentCreator
)
@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
)

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,13 +1,11 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.app.Service
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.ContextCompat
@ -17,21 +15,33 @@ import androidx.recyclerview.widget.RecyclerView
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.pointclick.sdk.offerwall.core.PointClickAd
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllActivity
import kr.co.vividnext.sodalive.audio_content.all.AudioContentRankingAllActivity
import kr.co.vividnext.sodalive.audio_content.all.by_theme.AudioContentAllByThemeActivity
import kr.co.vividnext.sodalive.audio_content.curation.AudioContentCurationActivity
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerViewModel
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationAdapter
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationViewModel
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentViewModel
import kr.co.vividnext.sodalive.audio_content.main.order.AudioContentMainOrderListViewModel
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingAdapter
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingViewModel
import kr.co.vividnext.sodalive.audio_content.main.recommend_series.AudioContentMainRecommendSeriesViewModel
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity
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.profile.UserProfileActivity
import kr.co.vividnext.sodalive.explorer.profile.series.UserProfileSeriesListAdapter
import kr.co.vividnext.sodalive.extensions.dpToPx
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
@ -40,32 +50,37 @@ import kotlin.math.roundToInt
class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
FragmentAudioContentMainBinding::inflate
) {
private val viewModel: AudioContentMainViewModel by inject()
private val recommendSeriesViewModel: AudioContentMainRecommendSeriesViewModel by inject()
private lateinit var seriesAdapter: UserProfileSeriesListAdapter
private lateinit var loadingDialog: LoadingDialog
private lateinit var imm: InputMethodManager
private lateinit var newContentCreatorAdapter: AudioContentMainNewContentCreatorAdapter
private val bannerViewModel: AudioContentMainBannerViewModel by inject()
private lateinit var bannerAdapter: AudioContentMainBannerAdapter
private val orderListViewModel: AudioContentMainOrderListViewModel by inject()
private lateinit var orderListAdapter: AudioContentMainContentAdapter
private val newContentViewModel: AudioContentMainNewContentViewModel by inject()
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentAdapter: AudioContentMainContentAdapter
private val contentRankingViewModel: AudioContentMainRankingViewModel by inject()
private lateinit var contentRankingSortAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var contentRankingAdapter: AudioContentMainRankingAdapter
private val curationViewModel: AudioContentMainCurationViewModel by inject()
private lateinit var curationAdapter: AudioContentMainCurationAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireContext().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
setupView()
bindData()
viewModel.getMain()
curationViewModel.getCurationList()
bannerViewModel.getMainBannerList()
newContentViewModel.getThemeList()
newContentViewModel.getNewContentOfTheme("전체")
contentRankingViewModel.getContentRanking()
contentRankingViewModel.getContentRankingSortType()
recommendSeriesViewModel.getRecommendSeriesList()
}
private fun setupView() {
@ -83,7 +98,7 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
binding.llUploadContent.visibility = View.GONE
}
setupNewContentCreator()
setupRecommendSeries()
setupBanner()
setupOrderList()
setupNewContentTheme()
@ -92,30 +107,68 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
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.ivCanFree.setOnClickListener {
PointClickAd.showOfferwall(requireActivity(), "무료충전")
binding.llMorningCall.setOnClickListener {
startActivity(
Intent(requireContext(), AudioContentAllByThemeActivity::class.java).apply {
putExtra(Constants.EXTRA_THEME_ID, 12L)
}
)
}
binding.ivContentKeep.setOnClickListener {
startActivity(
Intent(
requireContext(),
AudioContentOrderListActivity::class.java
)
)
}
binding.ivAlarm.setOnClickListener {
startActivity(
Intent(
requireActivity(),
AlarmListActivity::class.java
)
)
}
}
private fun setupNewContentCreator() {
newContentCreatorAdapter = AudioContentMainNewContentCreatorAdapter {
val intent = Intent(requireContext(), UserProfileActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, it)
startActivity(intent)
}
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
)
binding.rvNewContentCreator.layoutManager = LinearLayoutManager(
context,
val recyclerView = binding.rvRecommendSeries
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContentCreator.addItemDecoration(object : RecyclerView.ItemDecoration() {
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
@ -127,23 +180,43 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 10.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
newContentCreatorAdapter.itemCount - 1 -> {
outRect.left = 10.7f.dpToPx().toInt()
seriesAdapter.itemCount - 1 -> {
outRect.right = 0
outRect.left = 6.7f.dpToPx().toInt()
}
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
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() {
@ -204,10 +277,25 @@ 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())
bannerViewModel.bannerLiveData.observe(viewLifecycleOwner) {
if (bannerAdapter.itemCount <= 0 && it.isEmpty()) {
binding.rvBanner.visibility = View.GONE
binding.indicatorBanner.visibility = View.GONE
} else {
binding.rvBanner.visibility = View.VISIBLE
binding.indicatorBanner.visibility = View.VISIBLE
binding.rvBanner.refreshData(it)
}
}
bannerViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupOrderList() {
@ -266,11 +354,26 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
binding.tvMyStashViewAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentOrderListActivity::class.java))
}
orderListViewModel.orderListLiveData.observe(viewLifecycleOwner) {
orderListAdapter.addItems(it)
binding.llMyStash.visibility = if (
orderListAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
orderListViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupNewContentTheme() {
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
viewModel.getNewContentOfTheme(theme = it)
newContentViewModel.getNewContentOfTheme(theme = it)
}
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
@ -308,6 +411,11 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
})
binding.rvNewContentTheme.adapter = newContentThemeAdapter
newContentViewModel.themeListLiveData.observe(viewLifecycleOwner) {
binding.llNewContent.visibility = View.VISIBLE
newContentThemeAdapter.addItems(it)
}
}
private fun setupNewContent() {
@ -367,11 +475,27 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
})
binding.rvNewContent.adapter = newContentAdapter
newContentViewModel.newContentListLiveData.observe(viewLifecycleOwner) {
newContentAdapter.addItems(it)
}
newContentViewModel.isLoading.observe(viewLifecycleOwner) {
binding.pbNewContent.visibility = if (it) {
View.VISIBLE
} else {
View.GONE
}
}
newContentViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupContentRankingSortType() {
contentRankingSortAdapter = AudioContentMainNewContentThemeAdapter {
viewModel.getContentRanking(sort = it)
contentRankingViewModel.getContentRanking(sort = it)
}
binding.rvContentRankingSort.layoutManager = LinearLayoutManager(
@ -409,8 +533,14 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
})
binding.rvContentRankingSort.adapter = contentRankingSortAdapter
contentRankingViewModel.contentRankingSortListLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
contentRankingSortAdapter.addItems(it)
}
}
@SuppressLint("SetTextI18n")
private fun setupContentRanking() {
binding.ivContentRankingAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentRankingAllActivity::class.java))
@ -449,6 +579,16 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
})
binding.rvContentRanking.adapter = contentRankingAdapter
contentRankingViewModel.contentRankingLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
binding.tvDate.text = "${it.startDate}~${it.endDate}"
contentRankingAdapter.addItems(it.items)
}
contentRankingViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupCuration() {
@ -511,85 +651,50 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
}
}
})
binding.rvCuration.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
curationViewModel.getCurationList()
}
}
})
binding.rvCuration.adapter = curationAdapter
}
@SuppressLint("SetTextI18n")
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
curationViewModel.curationListLiveData.observe(viewLifecycleOwner) {
if (curationViewModel.page == 2) {
curationAdapter.clear()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.newContentUploadCreatorListLiveData.observe(viewLifecycleOwner) {
newContentCreatorAdapter.addItems(it)
binding.rvNewContentCreator.visibility = if (
newContentCreatorAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
viewModel.bannerLiveData.observe(viewLifecycleOwner) {
if (bannerAdapter.itemCount <= 0 && it.isEmpty()) {
binding.rvBanner.visibility = View.GONE
binding.indicatorBanner.visibility = View.GONE
} else {
binding.rvBanner.visibility = View.VISIBLE
binding.indicatorBanner.visibility = View.VISIBLE
binding.rvBanner.refreshData(it)
}
}
viewModel.orderListLiveData.observe(viewLifecycleOwner) {
orderListAdapter.addItems(it)
binding.llMyStash.visibility = if (
orderListAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
viewModel.newContentListLiveData.observe(viewLifecycleOwner) {
newContentAdapter.addItems(it)
}
viewModel.themeListLiveData.observe(viewLifecycleOwner) {
binding.llNewContent.visibility = View.VISIBLE
newContentThemeAdapter.addItems(it)
}
viewModel.curationListLiveData.observe(viewLifecycleOwner) {
curationAdapter.addItems(it)
binding.rvCuration.visibility = if (
curationAdapter.itemCount <= 0 && it.isEmpty()
) {
binding.rvCuration.visibility = if (curationAdapter.itemCount <= 0 && it.isEmpty()) {
View.GONE
} else {
View.VISIBLE
}
}
viewModel.contentRankingSortListLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
contentRankingSortAdapter.addItems(it)
curationViewModel.isLoading.observe(viewLifecycleOwner) {
binding.pbCuration.visibility = if (it) {
View.VISIBLE
} else {
View.GONE
}
}
viewModel.contentRankingLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
binding.tvDate.text = "${it.startDate}~${it.endDate}"
contentRankingAdapter.addItems(it.items)
curationViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
}

View File

@ -1,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,41 +1,36 @@
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>,
@SerializedName("contentRankingSortTypeList") val contentRankingSortTypeList: List<String>,
@SerializedName("contentRanking") val contentRanking: GetAudioContentRanking
)
@Keep
data class GetNewContentUploadCreator(
@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("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,
@ -47,6 +42,7 @@ data class GetAudioContentRankingItem(
@SerializedName("creatorNickname") val creatorNickname: String
)
@Keep
data class GetAudioContentCurationResponse(
@SerializedName("curationId") val curationId: Long,
@SerializedName("title") val title: String,
@ -54,6 +50,7 @@ data class GetAudioContentCurationResponse(
@SerializedName("contents") val audioContents: List<GetAudioContentMainItem>
)
@Keep
data class GetAudioContentBannerResponse(
@SerializedName("type") val type: AudioContentBannerType,
@SerializedName("thumbnailImageUrl") val thumbnailImageUrl: String,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.audio_content.main
package kr.co.vividnext.sodalive.audio_content.main.new_content
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@ -6,10 +6,11 @@ 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 AudioContentMainViewModel(
class AudioContentMainNewContentViewModel(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
@ -20,60 +21,24 @@ class AudioContentMainViewModel(
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _newContentUploadCreatorListLiveData =
MutableLiveData<List<GetNewContentUploadCreator>>()
val newContentUploadCreatorListLiveData: LiveData<List<GetNewContentUploadCreator>>
get() = _newContentUploadCreatorListLiveData
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _newContentListLiveData
private var _bannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
val bannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
get() = _bannerLiveData
private var _orderListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val orderListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _orderListLiveData
private var _themeListLiveData = MutableLiveData<List<String>>()
val themeListLiveData: LiveData<List<String>>
get() = _themeListLiveData
private var _curationListLiveData = MutableLiveData<List<GetAudioContentCurationResponse>>()
val curationListLiveData: LiveData<List<GetAudioContentCurationResponse>>
get() = _curationListLiveData
private var _contentRankingSortListLiveData = MutableLiveData<List<String>>()
val contentRankingSortListLiveData: LiveData<List<String>>
get() = _contentRankingSortListLiveData
private var _contentRankingLiveData = MutableLiveData<GetAudioContentRanking>()
val contentRankingLiveData: LiveData<GetAudioContentRanking>
get() = _contentRankingLiveData
fun getMain() {
_isLoading.value = true
fun getThemeList() {
compositeDisposable.add(
repository.getMain(token = "Bearer ${SharedPreferenceManager.token}")
repository.getNewContentThemeList(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val data = it.data
_newContentUploadCreatorListLiveData.value =
data.newContentUploadCreatorList
_newContentListLiveData.value = data.newContentList
_orderListLiveData.value = data.orderList
_bannerLiveData.value = data.bannerList
_curationListLiveData.value = data.curationList
_contentRankingLiveData.value = data.contentRanking
_contentRankingSortListLiveData.value = data.contentRankingSortTypeList
val themeList = listOf("전체").union(data.themeList).toList()
_themeListLiveData.value = themeList
val themeList = listOf("전체").union(it.data).toList()
_themeListLiveData.postValue(themeList)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
@ -96,6 +61,7 @@ class AudioContentMainViewModel(
}
fun getNewContentOfTheme(theme: String) {
_isLoading.value = true
compositeDisposable.add(
repository.getNewContentOfTheme(
theme = if (theme == "전체") {
@ -120,45 +86,13 @@ class AudioContentMainViewModel(
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun getContentRanking(sort: String) {
_isLoading.value = true
compositeDisposable.add(
repository.getContentRanking(
page = 1,
size = 12,
sortType = sort,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_isLoading.value = false
_contentRankingLiveData.value = it.data!!
} else {
_isLoading.value = false
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_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

@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.audio_content.main
package kr.co.vividnext.sodalive.audio_content.main.ranking
import android.annotation.SuppressLint
import android.view.LayoutInflater
@ -7,6 +7,7 @@ import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainRankingBinding
import kr.co.vividnext.sodalive.extensions.dpToPx

View File

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

View File

@ -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

@ -112,7 +112,7 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
private fun checkPermissions() {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
listOf(Manifest.permission.READ_MEDIA_AUDIO, Manifest.permission.READ_MEDIA_IMAGES)
listOf(Manifest.permission.READ_MEDIA_AUDIO)
} else {
listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}
@ -173,17 +173,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
R.color.color_eeeeee
)
)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentNo.visibility = View.GONE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llCommentNo.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
R.drawable.bg_round_corner_6_7_13181b_3bb9f1
)
} else {
binding.ivCommentNo.visibility = View.VISIBLE
@ -193,17 +193,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
R.color.color_eeeeee
)
)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentYes.visibility = View.GONE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llCommentYes
.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734_9970ff)
.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b_3bb9f1)
}
}
@ -223,17 +223,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
if (it) {
binding.ivAgeAll.visibility = View.GONE
binding.llAgeAll.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734
R.drawable.bg_round_corner_6_7_13181b
)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.ivAge19.visibility = View.VISIBLE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
@ -242,17 +242,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
)
} else {
binding.ivAge19.visibility = View.GONE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.ivAgeAll.visibility = View.VISIBLE
binding.llAgeAll.setBackgroundResource(
R.drawable.bg_round_corner_6_7_9970ff
R.drawable.bg_round_corner_6_7_3bb9f1
)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(

View File

@ -1,7 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.modify
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class ModifyAudioContentRequest(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String?,

View File

@ -4,14 +4,17 @@ import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.DialogAudioContentOrderConfirmBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kotlin.math.ceil
class AudioContentOrderConfirmDialog(
@ -58,16 +61,35 @@ class AudioContentOrderConfirmDialog(
}
dialogView.tvDuration.text = duration
dialogView.tvPrice.text = if (orderType == OrderType.RENTAL && !isOnlyRental) {
"${ceil(price * 0.6).toInt()}"
if (SharedPreferenceManager.userId == 17958L) {
dialogView.ivCan.visibility = View.GONE
dialogView.tvPrice.text = if (orderType == OrderType.RENTAL && !isOnlyRental) {
"${(ceil(price * 0.7).toInt() * 110).moneyFormat()}"
} else {
"${(price * 110).moneyFormat()}"
}
} else {
"$price"
dialogView.ivCan.visibility = View.VISIBLE
dialogView.tvPrice.text = if (orderType == OrderType.RENTAL && !isOnlyRental) {
ceil(price * 0.7).toInt().moneyFormat()
} else {
price.moneyFormat()
}
}
dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
"콘텐츠를 대여하시겠습니까?\n아래 캔이 차감됩니다."
if (SharedPreferenceManager.userId == 17958L) {
dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
"콘텐츠를 대여하시겠습니까?"
} else {
"콘텐츠를 소장하시겠습니까?"
}
} else {
"콘텐츠를 소장하시겠습니까?\n아래 캔이 차감됩니다."
dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
"콘텐츠를 대여하시겠습니까?\n아래 캔이 차감됩니다."
} else {
"콘텐츠를 소장하시겠습니까?\n아래 캔이 차감됩니다."
}
}
dialogView.tvCancel.setOnClickListener {

View File

@ -1,11 +1,14 @@
package kr.co.vividnext.sodalive.audio_content.order
import android.annotation.SuppressLint
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.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentOrderBinding
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kotlin.math.ceil
class AudioContentOrderFragment(
@ -26,15 +29,35 @@ class AudioContentOrderFragment(
return binding.root
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (SharedPreferenceManager.userId == 17958L) {
binding.tvKeepDate.text = "(이용기간 1년)"
binding.ivKeepCan.visibility = View.GONE
binding.ivRentalCan.visibility = View.GONE
} else {
binding.tvKeepDate.text = "(서비스 종료시까지)"
binding.ivKeepCan.visibility = View.VISIBLE
binding.ivRentalCan.visibility = View.VISIBLE
}
if (isOnlyRental) {
binding.tvRental.text = "$price"
if (SharedPreferenceManager.userId == 17958L) {
binding.tvRental.text = "${(price * 110).moneyFormat()}"
} else {
binding.tvRental.text = price.moneyFormat()
}
binding.rlKeep.visibility = View.GONE
} else {
binding.tvKeep.text = "$price"
binding.tvRental.text = "${ceil(price * 0.6).toInt()}"
if (SharedPreferenceManager.userId == 17958L) {
binding.tvKeep.text = "${(price * 110).moneyFormat()}"
binding.tvRental.text = "${(ceil(price * 0.7).toInt() * 110).moneyFormat()}"
} else {
binding.tvKeep.text = price.moneyFormat()
binding.tvRental.text = ceil(price * 0.7).toInt().moneyFormat()
}
binding.rlKeep.visibility = View.VISIBLE
binding.llKeep.setOnClickListener {

View File

@ -34,7 +34,7 @@ class AudioContentOrderListActivity : BaseActivity<ActivityAudioContentOrderList
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "구매목록"
binding.toolbar.tvBack.text = "콘텐츠 보관함"
binding.toolbar.tvBack.setOnClickListener { finish() }
adapter = AudioContentOrderListAdapter {

View File

@ -48,10 +48,11 @@ class AudioContentOrderListViewModel(
{
if (it.success && it.data != null) {
_totalCount.value = it.data.totalCount
page += 1
if (it.data.items.isNotEmpty()) {
page += 1
_orderList.postValue(it.data.items)
} else {
_orderList.postValue(listOf())
isLast = true
}
} else {

View File

@ -1,12 +1,15 @@
package kr.co.vividnext.sodalive.audio_content.order
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class GetAudioContentOrderListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentOrderListItem>
)
@Keep
data class GetAudioContentOrderListItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String,

View File

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

View File

@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.audio_content.series
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class GetSeriesListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<SeriesListItem>
) {
@Keep
data class SeriesListItem(
@SerializedName("seriesId") val seriesId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverImage") val coverImage: String,
@SerializedName("publishedDaysOfWeek") val publishedDaysOfWeek: String,
@SerializedName("isComplete") val isComplete: Boolean,
@SerializedName("creator") val creator: SeriesListItemCreator,
@SerializedName("numberOfContent") val numberOfContent: Int,
@SerializedName("isNew") val isNew: Boolean,
@SerializedName("isPopular") val isPopular: Boolean
)
@Keep
data class SeriesListItemCreator(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImage") val profileImage: String
)
}

View File

@ -0,0 +1,44 @@
package kr.co.vividnext.sodalive.audio_content.series
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audio_content.series.detail.GetSeriesContentListResponse
import kr.co.vividnext.sodalive.audio_content.series.detail.GetSeriesDetailResponse
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.settings.ContentType
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
import retrofit2.http.Query
interface SeriesApi {
@GET("/audio-content/series")
fun getSeriesList(
@Query("creatorId") creatorId: Long,
@Query("sortType") sortType: SeriesListAllViewModel.SeriesSortType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetSeriesListResponse>>
@GET("/audio-content/series/{id}")
fun getSeriesDetail(
@Path("id") seriesId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetSeriesDetailResponse>>
@GET("/audio-content/series/{id}/content")
fun getSeriesContentList(
@Path("id") seriesId: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sortType") sortType: SeriesListAllViewModel.SeriesSortType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetSeriesContentListResponse>>
@GET("/audio-content/series/recommend")
fun getRecommendSeriesList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
}

View File

@ -0,0 +1,104 @@
package kr.co.vividnext.sodalive.audio_content.series
import android.annotation.SuppressLint
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 coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemSeriesListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class SeriesListAdapter(
private val itemWidth: Int,
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit,
private val isVisibleCreator: Boolean
) : RecyclerView.Adapter<SeriesListAdapter.ViewHolder>() {
val items = mutableListOf<GetSeriesListResponse.SeriesListItem>()
inner class ViewHolder(
private val binding: ItemSeriesListBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(item: GetSeriesListResponse.SeriesListItem) {
val lp = binding.ivCover.layoutParams as ConstraintLayout.LayoutParams
lp.width = itemWidth
lp.height = itemWidth * 432 / 306
binding.ivCover.layoutParams = lp
binding.ivCover.load(item.coverImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5f.dpToPx()))
}
binding.tvTitle.text = item.title
binding.tvSeriesContentCount.text = "${item.numberOfContent}"
binding.tvPublishedDaysOfWeek.text = item.publishedDaysOfWeek
binding.tvNew.visibility = if (item.isNew) {
View.VISIBLE
} else {
View.GONE
}
binding.tvPopular.visibility = if (item.isPopular) {
View.VISIBLE
} else {
View.GONE
}
if (item.isComplete) {
binding.tvNew.visibility = View.GONE
binding.tvComplete.visibility = View.VISIBLE
} else {
binding.tvComplete.visibility = View.GONE
}
if (isVisibleCreator) {
binding.llCreator.visibility = View.VISIBLE
binding.tvCreator.text = item.creator.nickname
binding.ivCreator.load(item.creator.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.llCreator.setOnClickListener { onClickCreator(item.creator.creatorId) }
} else {
binding.llCreator.visibility = View.GONE
}
binding.root.setOnClickListener { onClickItem(item.seriesId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemSeriesListBinding.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(items: List<GetSeriesListResponse.SeriesListItem>) {
this.items.addAll(items)
notifyDataSetChanged()
}
fun clear() {
this.items.clear()
}
}

View File

@ -0,0 +1,118 @@
package kr.co.vividnext.sodalive.audio_content.series
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.DifferentSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivitySeriesListAllBinding
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
class SeriesListAllActivity : BaseActivity<ActivitySeriesListAllBinding>(
ActivitySeriesListAllBinding::inflate
) {
private val viewModel: SeriesListAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var seriesAdapter: SeriesListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val creatorId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
if (creatorId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
bindData()
viewModel.creatorId = creatorId
viewModel.getSeriesList()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "시리즈 전체보기"
binding.toolbar.tvBack.setOnClickListener { finish() }
seriesAdapter = SeriesListAdapter(
itemWidth = ((screenWidth - (13.3 * 3)) / 3).roundToInt(),
onClickItem = {
startActivity(
Intent(applicationContext, SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
},
onClickCreator = {},
isVisibleCreator = false
)
val spanCount = 3
val horizontalSpacing = 20
val verticalSpacing = 100
val recyclerView = binding.rvSeriesAll
recyclerView.layoutManager = GridLayoutManager(this, spanCount)
recyclerView.addItemDecoration(
DifferentSpacingItemDecoration(
spanCount = spanCount,
horizontalSpacing = horizontalSpacing,
verticalSpacing = verticalSpacing,
includeEdge = true
)
)
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getSeriesList()
}
}
})
recyclerView.adapter = seriesAdapter
}
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.seriesListLiveData.observe(this) {
if (viewModel.page - 1 == 1) {
seriesAdapter.clear()
binding.rvSeriesAll.scrollToPosition(0)
}
seriesAdapter.addItems(it)
}
}
}

View File

@ -0,0 +1,80 @@
package kr.co.vividnext.sodalive.audio_content.series
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.annotations.SerializedName
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class SeriesListAllViewModel(private val repository: SeriesRepository) : BaseViewModel() {
enum class SeriesSortType {
@SerializedName("NEWEST") NEWEST,
@SerializedName("OLDEST") OLDEST
}
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _seriesListLiveData = MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val seriesListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _seriesListLiveData
var creatorId = 0L
var isLast = false
var page = 1
private val size = 10
fun getSeriesList() {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getSeriesList(
creatorId = creatorId,
sortType = SeriesSortType.NEWEST,
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
page += 1
if (it.data.items.isNotEmpty()) {
_seriesListLiveData.value = it.data.items
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
}

View File

@ -0,0 +1,45 @@
package kr.co.vividnext.sodalive.audio_content.series
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.ContentType
class SeriesRepository(private val api: SeriesApi) {
fun getSeriesList(
creatorId: Long,
sortType: SeriesListAllViewModel.SeriesSortType,
page: Int,
size: Int,
token: String
) = api.getSeriesList(
creatorId = creatorId,
sortType = sortType,
page = page - 1,
size = size,
authHeader = token
)
fun getSeriesDetail(seriesId: Long, token: String) = api.getSeriesDetail(
seriesId = seriesId,
authHeader = token
)
fun getSeriesContentList(
seriesId: Long,
page: Int,
size: Int,
sortType: SeriesListAllViewModel.SeriesSortType,
token: String
) = api.getSeriesContentList(
seriesId = seriesId,
page = page - 1,
size = size,
sortType = sortType,
authHeader = token
)
fun getRecommendSeriesList(token: String) = api.getRecommendSeriesList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
}

View File

@ -0,0 +1,77 @@
package kr.co.vividnext.sodalive.audio_content.series.content
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.series.detail.GetSeriesContentListItem
import kr.co.vividnext.sodalive.databinding.ItemSeriesContentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class SeriesContentAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<SeriesContentAdapter.ViewHolder>() {
val items = mutableListOf<GetSeriesContentListItem>()
inner class ViewHolder(
private val binding: ItemSeriesContentBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetSeriesContentListItem) {
binding.ivCover.load(item.coverImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
binding.tvTitle.text = item.title
binding.tvDuration.text = item.duration
binding.tvPrice.visibility = View.GONE
binding.tvOwned.visibility = View.GONE
binding.tvRented.visibility = View.GONE
binding.tvPriceFree.visibility = View.GONE
if (item.isOwned) {
binding.tvOwned.visibility = View.VISIBLE
} else if (item.isRented) {
binding.tvRented.visibility = View.VISIBLE
} else if (item.price > 0) {
binding.tvPrice.text = "${item.price}"
binding.tvPrice.visibility = View.VISIBLE
} else {
binding.tvPriceFree.visibility = View.VISIBLE
}
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemSeriesContentBinding.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<GetSeriesContentListItem>) {
this.items.addAll(items)
notifyDataSetChanged()
}
fun clear() {
this.items.clear()
}
}

View File

@ -0,0 +1,157 @@
package kr.co.vividnext.sodalive.audio_content.series.content
import android.content.Intent
import android.os.Bundle
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllViewModel
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.ActivitySeriesContentAllBinding
import org.koin.android.ext.android.inject
class SeriesContentAllActivity : BaseActivity<ActivitySeriesContentAllBinding>(
ActivitySeriesContentAllBinding::inflate
) {
private val viewModel: SeriesContentAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: SeriesContentAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val seriesId = intent.getLongExtra(Constants.EXTRA_SERIES_ID, 0)
if (seriesId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
bindData()
viewModel.seriesId = seriesId
viewModel.getSeriesContentList()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
val seriesTitle = intent.getStringExtra(Constants.EXTRA_SERIES_TITLE) ?: ""
binding.toolbar.tvBack.text = if (seriesTitle.isNotBlank()) {
"$seriesTitle - 전체회차 듣기"
} else {
" 전체회차 듣기"
}
binding.toolbar.tvBack.setOnClickListener { finish() }
adapter = SeriesContentAdapter {
startActivity(
Intent(applicationContext, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
}
binding.rvSeriesContentAll.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
binding.rvSeriesContentAll.addItemDecoration(
DividerItemDecoration(
applicationContext,
DividerItemDecoration.VERTICAL
)
)
binding.rvSeriesContentAll.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.getSeriesContentList()
}
}
})
binding.rvSeriesContentAll.adapter = adapter
binding.tvSortNewest.setOnClickListener {
viewModel.changeSort(SeriesListAllViewModel.SeriesSortType.NEWEST)
}
binding.tvSortOldest.setOnClickListener {
viewModel.changeSort(SeriesListAllViewModel.SeriesSortType.OLDEST)
}
}
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.seriesContentListLiveData.observe(this) {
adapter.addItems(it)
}
viewModel.sortLiveData.observe(this) {
deselectSort()
selectSort(
when (it) {
SeriesListAllViewModel.SeriesSortType.OLDEST -> {
binding.tvSortOldest
}
else -> {
binding.tvSortNewest
}
}
)
}
}
private fun deselectSort() {
val color = ContextCompat.getColor(
applicationContext,
R.color.color_88e2e2e2
)
binding.tvSortNewest.setTextColor(color)
binding.tvSortOldest.setTextColor(color)
}
private fun selectSort(view: TextView) {
view.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_e2e2e2
)
)
adapter.clear()
viewModel.getSeriesContentList()
}
}

View File

@ -0,0 +1,87 @@
package kr.co.vividnext.sodalive.audio_content.series.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.series.SeriesListAllViewModel
import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository
import kr.co.vividnext.sodalive.audio_content.series.detail.GetSeriesContentListItem
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class SeriesContentAllViewModel(private val repository: SeriesRepository) : 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 _seriesContentListLiveData = MutableLiveData<List<GetSeriesContentListItem>>()
val seriesContentListLiveData: LiveData<List<GetSeriesContentListItem>>
get() = _seriesContentListLiveData
private var _sortLiveData = MutableLiveData(SeriesListAllViewModel.SeriesSortType.NEWEST)
val sortLiveData: LiveData<SeriesListAllViewModel.SeriesSortType>
get() = _sortLiveData
var seriesId = 0L
var page = 1
private var pageSize = 10
private var isLast = false
fun getSeriesContentList() {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getSeriesContentList(
seriesId = seriesId,
page = page,
size = pageSize,
sortType = _sortLiveData.value!!,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
page += 1
if (it.data.items.isNotEmpty()) {
_seriesContentListLiveData.value = it.data.items
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
fun changeSort(sortType: SeriesListAllViewModel.SeriesSortType) {
if (_sortLiveData.value != sortType) {
page = 1
isLast = false
_sortLiveData.value = sortType
}
}
}

View File

@ -0,0 +1,25 @@
package kr.co.vividnext.sodalive.audio_content.series.detail
import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Keep
data class GetSeriesContentListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetSeriesContentListItem>
)
@Parcelize
@Keep
data class GetSeriesContentListItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverImage") val coverImage: String,
@SerializedName("releaseDate") val releaseDate: String,
@SerializedName("duration") val duration: String,
@SerializedName("price") val price: Int,
@SerializedName("isRented") var isRented: Boolean,
@SerializedName("isOwned") var isOwned: Boolean
) : Parcelable

View File

@ -0,0 +1,40 @@
package kr.co.vividnext.sodalive.audio_content.series.detail
import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
@Keep
data class GetSeriesDetailResponse(
@SerializedName("seriesId") val seriesId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverImage") val coverImage: String,
@SerializedName("introduction") val introduction: String,
@SerializedName("genre") val genre: String,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("writer") val writer: String?,
@SerializedName("studio") val studio: String?,
@SerializedName("publishedDate") val publishedDate: String,
@SerializedName("creator") val creator: GetSeriesDetailCreator,
@SerializedName("rentalMinPrice") var rentalMinPrice: Int,
@SerializedName("rentalMaxPrice") var rentalMaxPrice: Int,
@SerializedName("rentalPeriod") val rentalPeriod: Int,
@SerializedName("minPrice") var minPrice: Int,
@SerializedName("maxPrice") var maxPrice: Int,
@SerializedName("keywordList") var keywordList: List<String>,
@SerializedName("publishedDaysOfWeek") var publishedDaysOfWeek: String,
@SerializedName("contentList") val contentList: List<GetSeriesContentListItem>,
@SerializedName("contentCount") val contentCount: Int
) : Parcelable {
@Parcelize
@Keep
data class GetSeriesDetailCreator(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImage") val profileImage: String,
@SerializedName("isFollow") var isFollow: Boolean,
@SerializedName("isNotify") var isNotify: Boolean
) : Parcelable
}

View File

@ -0,0 +1,252 @@
package kr.co.vividnext.sodalive.audio_content.series.detail
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import coil.load
import coil.size.Scale
import coil.transform.BlurTransformation
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import com.google.android.material.chip.Chip
import com.google.android.material.shape.ShapeAppearanceModel
import com.google.android.material.tabs.TabLayout
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivitySeriesDetailBinding
import kr.co.vividnext.sodalive.explorer.profile.CreatorFollowNotifyFragment
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class SeriesDetailActivity : BaseActivity<ActivitySeriesDetailBinding>(
ActivitySeriesDetailBinding::inflate
) {
private val viewModel: SeriesDetailViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val seriesId = intent.getLongExtra(Constants.EXTRA_SERIES_ID, 0)
if (seriesId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
bindData()
viewModel.seriesId = seriesId
viewModel.getSeriesDetail()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.ivBack.setOnClickListener { finish() }
setupTab()
}
private fun setupTab() {
val tabs = binding.tabs
tabs.addTab(tabs.newTab().setText("").setTag("home"))
tabs.addTab(tabs.newTab().setText("작품소개").setTag("introduction"))
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
val tag = tab.tag as String
changeFragment(tag)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
}
private fun changeFragment(tag: String) {
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
val fragment = when (tag) {
"introduction" -> SeriesDetailIntroductionFragment()
else -> SeriesDetailHomeFragment()
}
val bundle = Bundle()
bundle.putParcelable(Constants.EXTRA_SERIES, viewModel.seriesDetailResponse)
fragment.arguments = bundle
fragmentTransaction.replace(R.id.container, fragment, tag)
fragmentTransaction.setPrimaryNavigationFragment(fragment)
fragmentTransaction.setReorderingAllowed(true)
fragmentTransaction.commitNow()
}
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.seriesDetailLiveData.observe(this) {
setSeriesBg(it.coverImage)
setSeriesInfo(it)
setSeriesCreator(it.creator)
setSeriesKeywordChipList(it.keywordList)
changeFragment("home")
}
}
private fun setSeriesKeywordChipList(keywordList: List<String>) {
binding.chipGroup.isSingleLine = true
binding.chipGroup.isHorizontalScrollBarEnabled = false
for (keyword in keywordList) {
val chip = Chip(this)
chip.text = keyword
chip.isClickable = false
chip.isCheckable = false
chip.textSize = 12f
chip.chipStrokeWidth = 0f
chip.setChipBackgroundColorResource(R.color.color_222222)
val shapeAppearanceModel = ShapeAppearanceModel.Builder()
.setAllCornerSizes(26.7f.dpToPx())
.build()
chip.shapeAppearanceModel = shapeAppearanceModel
chip.setPadding(0, 0, 0, 0)
chip.setTextColor(ContextCompat.getColor(applicationContext, R.color.color_d2d2d2))
chip.typeface = ResourcesCompat.getFont(applicationContext, R.font.gmarket_sans_medium)
binding.chipGroup.addView(chip)
}
}
@SuppressLint("SetTextI18n")
private fun setSeriesInfo(seriesDetail: GetSeriesDetailResponse) {
binding.ivCover.load(seriesDetail.coverImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5f.dpToPx()))
}
binding.tvTitle.text = seriesDetail.title
binding.tvGenre.text = seriesDetail.genre
binding.tvPublishedDaysOfWeek.text = "${seriesDetail.publishedDaysOfWeek} 연재"
if (seriesDetail.isAdult) {
binding.tvAge19.visibility = View.VISIBLE
binding.tvAgeAll.visibility = View.GONE
} else {
binding.tvAge19.visibility = View.GONE
binding.tvAgeAll.visibility = View.VISIBLE
}
}
private fun setSeriesCreator(creator: GetSeriesDetailResponse.GetSeriesDetailCreator) {
binding.tvNickname.text = creator.nickname
binding.ivProfile.load(creator.profileImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
if (SharedPreferenceManager.userId != creator.creatorId) {
binding.ivFollow.visibility = View.VISIBLE
if (creator.isFollow) {
binding.ivFollow.setImageResource(
if (creator.isNotify) {
R.drawable.btn_following_big
} else {
R.drawable.btn_following_no_alarm_big
}
)
} else {
binding.ivFollow.setImageResource(R.drawable.btn_follow_big)
}
} else {
binding.ivFollow.visibility = View.GONE
}
binding.ivFollow.setOnClickListener {
if (creator.isFollow) {
val notifyFragment = CreatorFollowNotifyFragment(
onClickNotifyAll = {
viewModel.follow(
creatorId = creator.creatorId,
follow = true,
notify = true
) {
creator.isFollow = true
creator.isNotify = true
binding.ivFollow.setImageResource(R.drawable.btn_following_big)
}
},
onClickNotifyNone = {
viewModel.follow(
creatorId = creator.creatorId,
follow = true,
notify = false
) {
creator.isFollow = true
creator.isNotify = false
binding.ivFollow.setImageResource(R.drawable.btn_following_no_alarm_big)
}
},
onClickUnFollow = {
viewModel.follow(
creatorId = creator.creatorId,
follow = false,
notify = false
) {
creator.isFollow = false
creator.isNotify = false
binding.ivFollow.setImageResource(R.drawable.btn_follow_big)
}
}
)
if (notifyFragment.isAdded) return@setOnClickListener
notifyFragment.show(supportFragmentManager, notifyFragment.tag)
} else {
viewModel.follow(creatorId = creator.creatorId) {
creator.isFollow = true
creator.isNotify = true
binding.ivFollow.setImageResource(R.drawable.btn_following_big)
}
}
}
}
private fun setSeriesBg(coverImage: String) {
binding.ivBg.load(coverImage) {
transformations(
BlurTransformation(
this@SeriesDetailActivity,
25f,
2.5f
)
)
scale(Scale.FILL)
}
}
}

View File

@ -0,0 +1,83 @@
package kr.co.vividnext.sodalive.audio_content.series.detail
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.content.SeriesContentAdapter
import kr.co.vividnext.sodalive.audio_content.series.content.SeriesContentAllActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.databinding.FragmentSeriesDetailHomeBinding
class SeriesDetailHomeFragment : BaseFragment<FragmentSeriesDetailHomeBinding>(
FragmentSeriesDetailHomeBinding::inflate
) {
private var seriesDetailResponse: GetSeriesDetailResponse? = null
private lateinit var adapter: SeriesContentAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments != null) {
seriesDetailResponse = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requireArguments().getParcelable(
Constants.EXTRA_SERIES,
GetSeriesDetailResponse::class.java
)
} else {
requireArguments().getParcelable(Constants.EXTRA_SERIES)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (seriesDetailResponse != null) {
setContent()
}
}
@SuppressLint("SetTextI18n")
private fun setContent() {
binding.tvTotalCount.text = "(${seriesDetailResponse!!.contentCount})"
binding.llContentAll.setOnClickListener {
startActivity(
Intent(requireActivity(), SeriesContentAllActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, seriesDetailResponse!!.seriesId)
putExtra(Constants.EXTRA_SERIES_TITLE, seriesDetailResponse!!.title)
}
)
}
adapter = SeriesContentAdapter {
startActivity(
Intent(requireActivity(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
}
binding.rvContent.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.VERTICAL,
false
)
binding.rvContent.addItemDecoration(
DividerItemDecoration(
requireContext(),
DividerItemDecoration.VERTICAL
)
)
binding.rvContent.adapter = adapter
adapter.addItems(seriesDetailResponse!!.contentList)
}
}

View File

@ -0,0 +1,142 @@
package kr.co.vividnext.sodalive.audio_content.series.detail
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.shape.ShapeAppearanceModel
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.databinding.FragmentSeriesDetailIntroductionBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class SeriesDetailIntroductionFragment : BaseFragment<FragmentSeriesDetailIntroductionBinding>(
FragmentSeriesDetailIntroductionBinding::inflate
) {
private var seriesDetailResponse: GetSeriesDetailResponse? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments != null) {
seriesDetailResponse = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requireArguments().getParcelable(
Constants.EXTRA_SERIES,
GetSeriesDetailResponse::class.java
)
} else {
requireArguments().getParcelable(Constants.EXTRA_SERIES)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (seriesDetailResponse != null) {
setSeriesKeywordChipList(seriesDetailResponse!!.keywordList)
setSeriesIntroduction(seriesDetailResponse!!.introduction)
setSeriesPrice()
setSeriesInfo()
}
}
private fun setSeriesPrice() {
val rentalMinPrice = seriesDetailResponse!!.rentalMinPrice
val rentalMaxPrice = seriesDetailResponse!!.rentalMaxPrice
val minPrice = seriesDetailResponse!!.minPrice
val maxPrice = seriesDetailResponse!!.maxPrice
binding.tvRentalPrice.text = if (rentalMinPrice == rentalMaxPrice) {
if (rentalMaxPrice == 0) {
"무료(15일)"
} else {
"$rentalMaxPrice(15일)"
}
} else {
"${if (rentalMinPrice == 0) "무료" else rentalMinPrice} ~ ${rentalMaxPrice}캔 (15일)"
}
binding.tvPrice.text = if (minPrice == maxPrice) {
if (maxPrice == 0) {
"무료"
} else {
"$maxPrice"
}
} else {
"${if (minPrice == 0) "무료" else minPrice} ~ ${maxPrice}"
}
}
@SuppressLint("SetTextI18n")
private fun setSeriesInfo() {
binding.tvGenre.text = seriesDetailResponse!!.genre
binding.tvIsAdult.text = if (seriesDetailResponse!!.isAdult) {
"19세 이상"
} else {
"전체연령가"
}
binding.tvPublishedDate.text = seriesDetailResponse!!.publishedDate
binding.tvPublishedDaysOfWeek.text =
if (seriesDetailResponse!!.publishedDaysOfWeek == "랜덤") {
seriesDetailResponse!!.publishedDaysOfWeek
} else {
seriesDetailResponse!!.publishedDaysOfWeek
}
if (seriesDetailResponse!!.writer != null) {
binding.tvWriter.visibility = View.VISIBLE
binding.tvWriterLabel.visibility = View.VISIBLE
binding.tvWriter.text = seriesDetailResponse!!.writer
} else {
binding.tvWriter.visibility = View.GONE
binding.tvWriterLabel.visibility = View.GONE
}
if (seriesDetailResponse!!.studio != null) {
binding.tvStudio.visibility = View.VISIBLE
binding.tvStudioLabel.visibility = View.VISIBLE
binding.tvStudio.text = seriesDetailResponse!!.studio
} else {
binding.tvStudio.visibility = View.GONE
binding.tvStudioLabel.visibility = View.GONE
}
}
private fun setSeriesIntroduction(introduction: String) {
binding.tvIntroduce.text = introduction
}
private fun setSeriesKeywordChipList(keywordList: List<String>) {
binding.chipGroup.isHorizontalScrollBarEnabled = false
for (keyword in keywordList) {
val chip = Chip(requireActivity())
chip.text = keyword
chip.isClickable = false
chip.isCheckable = false
chip.textSize = 12f
chip.chipStrokeWidth = 0f
chip.setChipBackgroundColorResource(R.color.color_222222)
val shapeAppearanceModel = ShapeAppearanceModel.Builder()
.setAllCornerSizes(26.7f.dpToPx())
.build()
chip.shapeAppearanceModel = shapeAppearanceModel
chip.setEnsureMinTouchTargetSize(false)
chip.setPadding(0, 0, 0, 0)
chip.setTextColor(ContextCompat.getColor(requireContext(), R.color.color_d2d2d2))
chip.typeface = ResourcesCompat.getFont(requireContext(), R.font.gmarket_sans_medium)
binding.chipGroup.addView(chip)
}
}
}

View File

@ -0,0 +1,105 @@
package kr.co.vividnext.sodalive.audio_content.series.detail
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.SeriesRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.user.UserRepository
class SeriesDetailViewModel(
private val repository: SeriesRepository,
private val userRepository: UserRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _seriesDetailLiveData = MutableLiveData<GetSeriesDetailResponse>()
val seriesDetailLiveData: LiveData<GetSeriesDetailResponse>
get() = _seriesDetailLiveData
var seriesId = 0L
lateinit var seriesDetailResponse: GetSeriesDetailResponse
fun getSeriesDetail() {
_isLoading.value = true
compositeDisposable.add(
repository.getSeriesDetail(
seriesId = seriesId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
seriesDetailResponse = it.data
_seriesDetailLiveData.value = seriesDetailResponse
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun follow(
creatorId: Long,
follow: Boolean = true,
notify: Boolean = true,
onSuccess: () -> Unit
) {
_isLoading.value = true
compositeDisposable.add(
userRepository.creatorFollow(
creatorId = creatorId,
follow,
notify,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.audio_content.upload
import android.Manifest
import android.annotation.SuppressLint
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Intent
import android.net.Uri
import android.os.Build
@ -28,9 +30,15 @@ import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentUploadBinding
import kr.co.vividnext.sodalive.dialog.LiveDialog
import kr.co.vividnext.sodalive.dialog.SodaLiveTimePickerDialog
import kr.co.vividnext.sodalive.extensions.convertDateFormat
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBinding>(
ActivityAudioContentUploadBinding::inflate
@ -111,6 +119,26 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
}
}
private val datePickerDialogListener =
DatePickerDialog.OnDateSetListener { _, year, monthOfYear, dayOfMonth ->
viewModel.releaseDate = String.format("%d-%02d-%02d", year, monthOfYear + 1, dayOfMonth)
viewModel.setReservationDate(
String.format(
"%d.%02d.%02d",
year,
monthOfYear + 1,
dayOfMonth
)
)
}
private val timePickerDialogListener =
TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute ->
val timeString = String.format("%02d:%02d", hourOfDay, minute)
viewModel.releaseTime = timeString
viewModel.setReservationTime(timeString.convertDateFormat("HH:mm", "a hh:mm"))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkPermissions()
@ -125,6 +153,11 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.tvServiceDate.text = if (SharedPreferenceManager.userId == 17958L) {
"※ 이용기간 : 대여(15일) | 소장(이용 기간 1년)"
} else {
"※ 이용기간 : 대여(15일) | 소장(서비스 종료시까지)"
}
binding.toolbar.tvBack.text = "콘텐츠 등록"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.llTheme.setOnClickListener {
@ -169,10 +202,16 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
binding.llPricePaid.setOnClickListener { viewModel.setPriceFree(false) }
binding.llPriceFree.setOnClickListener { viewModel.setPriceFree(true) }
binding.llPreviewYes.setOnClickListener { viewModel.setGeneratePreview(true) }
binding.llPreviewNo.setOnClickListener { viewModel.setGeneratePreview(false) }
binding.llLimited.setOnClickListener { viewModel.setLimited(true) }
binding.llNotLimited.setOnClickListener { viewModel.setLimited(false) }
binding.llRentalAndKeep.setOnClickListener { viewModel.setIsOnlyRental(false) }
binding.llOnlyRental.setOnClickListener { viewModel.setIsOnlyRental(true) }
binding.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) }
binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) }
binding.llActiveNow.setOnClickListener { viewModel.setActiveReservation(false) }
binding.llActiveReservation.setOnClickListener { viewModel.setActiveReservation(true) }
binding.tvCancel.setOnClickListener { finish() }
binding.tvUpload.setOnClickListener {
@ -189,11 +228,76 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
).show(screenWidth)
}
}
binding.tvReservationDate.setOnClickListener {
val reservationDate = viewModel.releaseDate.split("-")
val datePicker: DatePickerDialog
if (reservationDate.isNotEmpty() && reservationDate.size == 3) {
datePicker = DatePickerDialog(
this,
R.style.DatePickerStyle,
datePickerDialogListener,
reservationDate[0].toInt(),
reservationDate[1].toInt() - 1,
reservationDate[2].toInt()
)
} else {
val dateString = SimpleDateFormat(
"yyyy.MM.dd",
Locale.getDefault()
).format(Date()).split(".")
datePicker = DatePickerDialog(
this,
R.style.DatePickerStyle,
datePickerDialogListener,
dateString[0].toInt(),
dateString[1].toInt() - 1,
dateString[2].toInt()
)
}
datePicker.show()
}
binding.tvReservationTime.setOnClickListener {
val reservationTime = viewModel.releaseTime.split(":")
val timePicker: TimePickerDialog
if (reservationTime.isNotEmpty() && reservationTime.size == 2) {
timePicker = SodaLiveTimePickerDialog(
this,
R.style.TimePickerStyle,
timePickerDialogListener,
reservationTime[0].toInt(),
reservationTime[1].toInt(),
false
)
} else {
val calendar = Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, 1)
}
val initialHour = calendar.get(Calendar.HOUR_OF_DAY)
val initialMinute = calendar.get(Calendar.MINUTE) / 15 * 15
timePicker = SodaLiveTimePickerDialog(
this,
R.style.TimePickerStyle,
timePickerDialogListener,
initialHour,
initialMinute,
false
)
}
timePicker.show()
}
}
private fun checkPermissions() {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
listOf(Manifest.permission.READ_MEDIA_AUDIO, Manifest.permission.READ_MEDIA_IMAGES)
listOf(Manifest.permission.READ_MEDIA_AUDIO)
} else {
listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}
@ -260,6 +364,24 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
}
)
compositeDisposable.add(
binding.etLimited.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
val limited = it.toString().toIntOrNull()
if (limited != null) {
viewModel.limited = limited.toInt()
} else {
viewModel.limited = 0
if (it.isNotBlank()) {
binding.etLimited.setText(it.substring(0, it.length - 1))
binding.etLimited.setSelection(it.length - 1)
}
}
}
)
compositeDisposable.add(
binding.etPreviewStartTime.textChanges().skip(1)
.subscribeOn(Schedulers.io())
@ -306,6 +428,22 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
}
}
viewModel.isLimitedLiveData.observe(this) {
if (it) {
checkLimited()
} else {
checkNotLimited()
}
}
viewModel.isGeneratePreviewLiveData.observe(this) {
if (it) {
checkGeneratePreview()
} else {
checkNotGeneratePreview()
}
}
viewModel.isOnlyRentalLiveData.observe(this) {
if (it) {
checkOnlyRental()
@ -323,17 +461,17 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
R.color.color_eeeeee
)
)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentNo.visibility = View.GONE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llCommentNo.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
R.drawable.bg_round_corner_6_7_13181b
)
} else {
binding.ivCommentNo.visibility = View.VISIBLE
@ -343,17 +481,17 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
R.color.color_eeeeee
)
)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentYes.visibility = View.GONE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llCommentYes
.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734_9970ff)
.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
}
}
@ -369,16 +507,16 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
viewModel.isAdultLiveData.observe(this) {
if (it) {
binding.ivAgeAll.visibility = View.GONE
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.ivAge19.visibility = View.VISIBLE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
@ -387,16 +525,16 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
)
} else {
binding.ivAge19.visibility = View.GONE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.ivAgeAll.visibility = View.VISIBLE
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
@ -406,6 +544,81 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
}
}
}
viewModel.isActiveReservationLiveData.observe(this) {
if (it) {
checkActiveReservation()
} else {
checkActiveNow()
}
}
viewModel.reservationDateLiveData.observe(this) {
binding.tvReservationDate.text = it
}
viewModel.reservationTimeLiveData.observe(this) {
binding.tvReservationTime.text = it
}
viewModel.isShowConfigLimitedLiveData.observe(this) {
if (it) {
binding.llConfigLimited.visibility = View.VISIBLE
binding.tvConfigLimitedTitle.visibility = View.VISIBLE
} else {
binding.llConfigLimited.visibility = View.GONE
binding.tvConfigLimitedTitle.visibility = View.GONE
binding.etLimited.visibility = View.GONE
binding.etLimited.setText("")
}
}
}
private fun checkActiveNow() {
binding.llReservationDatetime.visibility = View.GONE
binding.ivActiveNow.visibility = View.VISIBLE
binding.tvActiveNow.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llActiveNow.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivActiveReservation.visibility = View.GONE
binding.tvActiveReservation.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.llActiveReservation.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
}
private fun checkActiveReservation() {
binding.llReservationDatetime.visibility = View.VISIBLE
binding.ivActiveReservation.visibility = View.VISIBLE
binding.tvActiveReservation.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llActiveReservation.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivActiveNow.visibility = View.GONE
binding.tvActiveNow.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.llActiveNow.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
}
private fun checkPriceFree() {
@ -422,19 +635,20 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
R.color.color_eeeeee
)
)
binding.llPriceFree.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llPriceFree.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivPricePaid.visibility = View.GONE
binding.tvPricePaid.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llPricePaid.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
R.drawable.bg_round_corner_6_7_13181b
)
binding.llConfigPreview.visibility = View.GONE
binding.llConfigPreviewTime.visibility = View.GONE
}
@ -450,21 +664,114 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
R.color.color_eeeeee
)
)
binding.llPricePaid.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llPricePaid.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivPriceFree.visibility = View.GONE
binding.tvPriceFree.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llPriceFree.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
R.drawable.bg_round_corner_6_7_13181b
)
binding.llConfigPreview.visibility = View.VISIBLE
}
private fun checkNotLimited() {
binding.ivNotLimited.visibility = View.VISIBLE
binding.tvNotLimited.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llNotLimited.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivLimited.visibility = View.GONE
binding.tvLimited.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.llLimited.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
binding.etLimited.visibility = View.GONE
binding.etLimited.setText("")
}
private fun checkLimited() {
binding.ivLimited.visibility = View.VISIBLE
binding.tvLimited.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llLimited.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivNotLimited.visibility = View.GONE
binding.tvNotLimited.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.llNotLimited.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
binding.etLimited.visibility = View.VISIBLE
}
private fun checkGeneratePreview() {
binding.ivPreviewYes.visibility = View.VISIBLE
binding.tvPreviewYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llPreviewYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivPreviewNo.visibility = View.GONE
binding.tvPreviewNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.llPreviewNo.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
binding.llConfigPreviewTime.visibility = View.VISIBLE
}
private fun checkNotGeneratePreview() {
binding.ivPreviewNo.visibility = View.VISIBLE
binding.tvPreviewNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llPreviewNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivPreviewYes.visibility = View.GONE
binding.tvPreviewYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.llPreviewYes.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
binding.llConfigPreviewTime.visibility = View.GONE
}
private fun checkRentalAndKeep() {
binding.tvPriceTitle.text = "소장 가격"
binding.ivRentalAndKeep.visibility = View.VISIBLE
@ -474,17 +781,17 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
R.color.color_eeeeee
)
)
binding.llRentalAndKeep.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llRentalAndKeep.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivOnlyRental.visibility = View.GONE
binding.tvOnlyRental.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llOnlyRental.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
R.drawable.bg_round_corner_6_7_13181b
)
}
@ -497,17 +804,17 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
R.color.color_eeeeee
)
)
binding.llOnlyRental.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llOnlyRental.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivRentalAndKeep.visibility = View.GONE
binding.tvRentalAndKeep.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llRentalAndKeep.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
R.drawable.bg_round_corner_6_7_13181b
)
}

View File

@ -20,6 +20,7 @@ import okio.BufferedSink
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class AudioContentUploadViewModel(
private val repository: AudioContentRepository
@ -49,12 +50,39 @@ class AudioContentUploadViewModel(
val isPriceFreeLiveData: LiveData<Boolean>
get() = _isPriceFreeLiveData
private val _isLimitedLiveData = MutableLiveData(false)
val isLimitedLiveData: LiveData<Boolean>
get() = _isLimitedLiveData
private val _isShowConfigLimitedLiveData = MutableLiveData(false)
val isShowConfigLimitedLiveData: LiveData<Boolean>
get() = _isShowConfigLimitedLiveData
private val _isGeneratePreviewLiveData = MutableLiveData(true)
val isGeneratePreviewLiveData: LiveData<Boolean>
get() = _isGeneratePreviewLiveData
private val _isActiveReservationLiveData = MutableLiveData(false)
val isActiveReservationLiveData: LiveData<Boolean>
get() = _isActiveReservationLiveData
private val _reservationDateLiveData = MutableLiveData("날짜를 선택해주세요")
val reservationDateLiveData: LiveData<String>
get() = _reservationDateLiveData
private val _reservationTimeLiveData = MutableLiveData("시간을 설정해주세요")
val reservationTimeLiveData: LiveData<String>
get() = _reservationTimeLiveData
lateinit var getRealPathFromURI: (Uri) -> String?
var title = ""
var detail = ""
var tags = ""
var price = 0
var limited = 0
var releaseDate = ""
var releaseTime = ""
var theme: GetAudioContentThemeResponse? = null
var coverImageUri: Uri? = null
var contentUri: Uri? = null
@ -74,30 +102,94 @@ class AudioContentUploadViewModel(
if (isPriceFree) {
_isOnlyRentalLiveData.postValue(false)
_isShowConfigLimitedLiveData.postValue(false)
_isLimitedLiveData.postValue(false)
limited = 0
_isGeneratePreviewLiveData.postValue(true)
} else {
if (!_isOnlyRentalLiveData.value!!) {
_isShowConfigLimitedLiveData.postValue(true)
}
}
}
fun setGeneratePreview(isGeneratePreview: Boolean) {
_isGeneratePreviewLiveData.value = isGeneratePreview
}
fun setLimited(isLimited: Boolean) {
_isLimitedLiveData.value = isLimited
if (!isLimited) {
limited = 0
}
}
fun setIsOnlyRental(isOnlyRental: Boolean) {
_isOnlyRentalLiveData.postValue(isOnlyRental)
if (isOnlyRental) {
_isShowConfigLimitedLiveData.postValue(false)
_isLimitedLiveData.postValue(false)
limited = 0
} else {
if (!_isPriceFreeLiveData.value!!) {
_isShowConfigLimitedLiveData.postValue(true)
}
}
}
fun setActiveReservation(isActiveReservation: Boolean) {
_isActiveReservationLiveData.postValue(isActiveReservation)
}
fun uploadAudioContent(onSuccess: () -> Unit) {
if (!_isLoading.value!! && validateData()) {
_isLoading.postValue(true)
val isGeneratePreview = _isGeneratePreviewLiveData.value!!
val request = CreateAudioContentRequest(
title = title,
detail = detail,
tags = tags,
price = price,
limited = if (
price > 0 &&
limited > 0 &&
_isLimitedLiveData.value!! &&
_isShowConfigLimitedLiveData.value!! &&
!_isPriceFreeLiveData.value!!
) {
limited
} else {
null
},
releaseDate = if (_isActiveReservationLiveData.value!!) {
"$releaseDate $releaseTime"
} else {
null
},
timezone = TimeZone.getDefault().id,
themeId = theme!!.id,
isAdult = _isAdultLiveData.value!!,
isOnlyRental = _isOnlyRentalLiveData.value!!,
isGeneratePreview = isGeneratePreview,
isCommentAvailable = _isAvailableCommentLiveData.value!!,
previewStartTime = previewStartTime,
previewEndTime = previewEndTime
previewStartTime = if (isGeneratePreview) {
previewStartTime
} else {
null
},
previewEndTime = if (isGeneratePreview) {
previewEndTime
} else {
null
}
)
Logger.e("test - $request")
val requestJson = Gson().toJson(request)
val coverImage = if (coverImageUri != null) {
@ -254,9 +346,9 @@ class AudioContentUploadViewModel(
val timeDifference = timeDifference(previewStartTime!!, previewEndTime!!)
if (timeDifference < 30000) {
if (timeDifference < 15000) {
_toastLiveData.postValue(
"미리 듣기의 최소 시간은 30초 입니다."
"미리 듣기의 최소 시간은 15초 입니다."
)
return false
@ -281,6 +373,14 @@ class AudioContentUploadViewModel(
return false
}
if (
_isActiveReservationLiveData.value!! &&
(releaseDate.isBlank() || releaseTime.isBlank())
) {
_toastLiveData.postValue("예약날짜와 시간을 선택해주세요.")
return false
}
return true
}
@ -307,4 +407,12 @@ class AudioContentUploadViewModel(
return 0
}
}
fun setReservationDate(dateString: String) {
_reservationDateLiveData.postValue(dateString)
}
fun setReservationTime(timeString: String) {
_reservationTimeLiveData.postValue(timeString)
}
}

View File

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

View File

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

View File

@ -1,7 +1,9 @@
package kr.co.vividnext.sodalive.common
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class ApiResponse<T>(
@SerializedName("success") val success: Boolean,
@SerializedName("data") val data: T? = null,

View File

@ -11,7 +11,9 @@ object Constants {
const val PREF_NO_CHAT_ROOM = "pref_no_chat"
const val PREF_PUSH_TOKEN = "pref_push_token"
const val PREF_PROFILE_IMAGE = "pref_profile_image"
const val PREF_CONTENT_PREFERENCE = "pref_content_preference"
const val PREF_IS_CONTENT_PLAY_LOOP = "pref_is_content_play_loop"
const val PREF_IS_ADULT_CONTENT_VISIBLE = "pref_is_adult_content_visible"
const val PREF_IS_FOLLOWED_CREATOR_LIVE = "pref_is_followed_creator_live"
const val PREF_NOT_SHOWING_EVENT_POPUP_ID = "pref_not_showing_event_popup_id"
const val PREF_IS_VIEWED_ON_BOARDING_TUTORIAL = "pref_is_viewed_on_boarding_tutorial"
@ -19,16 +21,22 @@ object Constants {
const val EXTRA_CAN = "extra_can"
const val EXTRA_DATA = "extra_data"
const val EXTRA_TERMS = "extra_terms"
const val EXTRA_PRIVACY = "extra_privacy"
const val EXTRA_EVENT = "extra_event"
const val EXTRA_SERIES = "extra_series"
const val EXTRA_NOTICE = "extra_notice"
const val EXTRA_ROOM_ID = "extra_room_id"
const val EXTRA_USER_ID = "extra_user_id"
const val EXTRA_THEME_ID = "extra_theme_id"
const val EXTRA_SERIES_ID = "extra_series_id"
const val EXTRA_NICKNAME = "extra_nickname"
const val EXTRA_MESSAGE_ID = "extra_message_id"
const val EXTRA_ROOM_DETAIL = "extra_room_detail"
const val EXTRA_MESSAGE_BOX = "extra_message_box"
const val EXTRA_SERIES_TITLE = "extra_series_title"
const val EXTRA_TEXT_MESSAGE = "extra_text_message"
const val EXTRA_LIVE_TIME_NOW = "extra_live_time_now"
const val EXTRA_RESULT_ROULETTE = "extra_result_roulette"
const val EXTRA_GO_TO_PREV_PAGE = "extra_go_to_prev_page"
const val EXTRA_SELECT_RECIPIENT = "extra_select_recipient"
const val EXTRA_ROOM_CHANNEL_NAME = "extra_room_channel_name"
@ -47,13 +55,22 @@ object Constants {
const val EXTRA_AUDIO_CONTENT_COMMENT = "audio_content_comment"
const val EXTRA_AUDIO_CONTENT_LOADING = "audio_content_loading"
const val EXTRA_AUDIO_CONTENT_CREATOR_ID = "audio_content_creator_id"
const val EXTRA_AUDIO_CONTENT_CREATOR_NICKNAME = "audio_content_creator_nickname"
const val EXTRA_AUDIO_CONTENT_CURATION_ID = "extra_audio_content_curation_id"
const val EXTRA_AUDIO_CONTENT_CURATION_TITLE = "extra_audio_content_curation_title"
const val EXTRA_AUDIO_CONTENT_NEXT_ACTION = "audio_content_next_action"
const val EXTRA_AUDIO_CONTENT_ALERT_PREVIEW = "audio_content_alert_preview"
const val EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL = "audio_content_cover_image_url"
const val EXTRA_IS_SHOW_SECRET = "extra_is_show_secret"
const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2
const val ACTION_AUDIO_CONTENT_RECEIVER = "soda_live_action_content_receiver"
const val ACTION_MAIN_AUDIO_CONTENT_RECEIVER = "soda_live_action_main_content_receiver"
const val EXTRA_COMMUNITY_POST_ID = "community_post_id"
const val EXTRA_COMMUNITY_CREATOR_ID = "community_creator_id"
const val EXTRA_COMMUNITY_POST_COMMENT = "community_post_comment_id"
const val EXTRA_ALARM_ID = "alarm_id"
const val EXTRA_ROULETTE_AVAILABLE_ACTIVE = "roulette_available_active"
}

View File

@ -0,0 +1,18 @@
package kr.co.vividnext.sodalive.common
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class Converter {
@TypeConverter
fun fromString(value: String): List<String> {
val listType = object : TypeToken<List<String>>() {}.type
return Gson().fromJson(value, listType)
}
@TypeConverter
fun fromList(list: List<String>): String {
return Gson().toJson(list)
}
}

View File

@ -0,0 +1,39 @@
package kr.co.vividnext.sodalive.common
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class DifferentSpacingItemDecoration(
private val spanCount: Int,
private val horizontalSpacing: Int,
private val verticalSpacing: Int,
private val includeEdge: Boolean
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view) // 아이템의 위치
val column = position % spanCount // 아이템의 열 위치
if (includeEdge) {
outRect.left = horizontalSpacing - column * horizontalSpacing / spanCount
outRect.right = (column + 1) * horizontalSpacing / spanCount
if (position < spanCount) {
outRect.top = verticalSpacing
}
outRect.bottom = verticalSpacing
} else {
outRect.left = column * horizontalSpacing / spanCount
outRect.right = horizontalSpacing - (column + 1) * horizontalSpacing / spanCount
if (position >= spanCount) {
outRect.top = verticalSpacing
}
}
}
}

View File

@ -0,0 +1,37 @@
package kr.co.vividnext.sodalive.common
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
class GridSpacingItemDecoration(
private val spanCount: Int,
private val spacing: Int,
private val includeEdge: Boolean
) : ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view) // Item position
val column = position % spanCount // Current column
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount
outRect.right = (column + 1) * spacing / spanCount
if (position < spanCount) { // Top edge
outRect.top = spacing
}
outRect.bottom = spacing // Item bottom
} else {
outRect.left = column * spacing / spanCount
outRect.right = spacing - (column + 1) * spacing / spanCount
if (position >= spanCount) {
outRect.top = spacing // Item top
}
}
}
}

View File

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

View File

@ -95,6 +95,18 @@ object SharedPreferenceManager {
sharedPreferences[Constants.PREF_IS_ADULT] = value
}
var isAdultContentVisible: Boolean
get() = sharedPreferences[Constants.PREF_IS_ADULT_CONTENT_VISIBLE, true]
set(value) {
sharedPreferences[Constants.PREF_IS_ADULT_CONTENT_VISIBLE] = value
}
var contentPreference: Int
get() = sharedPreferences[Constants.PREF_CONTENT_PREFERENCE, 0]
set(value) {
sharedPreferences[Constants.PREF_CONTENT_PREFERENCE] = value
}
var pushToken: String
get() = sharedPreferences[Constants.PREF_PUSH_TOKEN, ""]
set(value) {

View File

@ -7,6 +7,8 @@ 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.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
@ -33,7 +35,19 @@ class SodaLiveService : Service() {
}
private fun updateNotification(content: String) {
startForeground(Constants.LIVE_SERVICE_NOTIFICATION_ID, createNotification(content))
val notification = createNotification(content)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val foregroundServiceTypes = FOREGROUND_SERVICE_TYPE_MICROPHONE or
FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
startForeground(
Constants.LIVE_SERVICE_NOTIFICATION_ID,
notification,
foregroundServiceTypes
)
} else {
startForeground(Constants.LIVE_SERVICE_NOTIFICATION_ID, notification)
}
}
private fun createNotification(content: String): Notification {
@ -62,7 +76,7 @@ class SodaLiveService : Service() {
)
val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId)
.setSmallIcon(R.drawable.ic_notification)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(getString(R.string.app_name))
.setContentText(content)
.setOngoing(true)

View File

@ -9,14 +9,26 @@ import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
import kr.co.vividnext.sodalive.audio_content.PlaybackTrackingRepository
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllViewModel
import kr.co.vividnext.sodalive.audio_content.all.AudioContentRankingAllViewModel
import kr.co.vividnext.sodalive.audio_content.all.by_theme.AudioContentAllByThemeViewModel
import kr.co.vividnext.sodalive.audio_content.category.CategoryApi
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentListViewModel
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentReplyViewModel
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentRepository
import kr.co.vividnext.sodalive.audio_content.curation.AudioContentCurationViewModel
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailViewModel
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainViewModel
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerViewModel
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationViewModel
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentViewModel
import kr.co.vividnext.sodalive.audio_content.main.order.AudioContentMainOrderListViewModel
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.modify.AudioContentModifyViewModel
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListViewModel
import kr.co.vividnext.sodalive.audio_content.series.SeriesApi
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllViewModel
import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository
import kr.co.vividnext.sodalive.audio_content.series.content.SeriesContentAllViewModel
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailViewModel
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadViewModel
import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeViewModel
import kr.co.vividnext.sodalive.common.ApiBuilder
@ -25,6 +37,12 @@ import kr.co.vividnext.sodalive.explorer.ExplorerApi
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.explorer.ExplorerViewModel
import kr.co.vividnext.sodalive.explorer.profile.UserProfileViewModel
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityApi
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllViewModel
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment.CreatorCommunityCommentListViewModel
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyViewModel
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteViewModel
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAllViewModel
import kr.co.vividnext.sodalive.explorer.profile.fantalk.UserProfileFantalkAllViewModel
import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListViewModel
@ -40,9 +58,15 @@ import kr.co.vividnext.sodalive.live.room.LiveRoomViewModel
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailViewModel
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageViewModel
import kr.co.vividnext.sodalive.live.room.menu.MenuApi
import kr.co.vividnext.sodalive.live.room.menu.MenuConfigRepository
import kr.co.vividnext.sodalive.live.room.menu.MenuConfigViewModel
import kr.co.vividnext.sodalive.live.room.tag.LiveTagRepository
import kr.co.vividnext.sodalive.live.room.tag.LiveTagViewModel
import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditViewModel
import kr.co.vividnext.sodalive.live.roulette.RouletteRepository
import kr.co.vividnext.sodalive.live.roulette.config.RouletteApi
import kr.co.vividnext.sodalive.live.roulette.config.RouletteSettingsViewModel
import kr.co.vividnext.sodalive.main.MainViewModel
import kr.co.vividnext.sodalive.message.MessageApi
import kr.co.vividnext.sodalive.message.MessageRepository
@ -53,12 +77,21 @@ import kr.co.vividnext.sodalive.message.text.TextMessageWriteViewModel
import kr.co.vividnext.sodalive.message.voice.VoiceMessageViewModel
import kr.co.vividnext.sodalive.message.voice.VoiceMessageWriteViewModel
import kr.co.vividnext.sodalive.mypage.MyPageViewModel
import kr.co.vividnext.sodalive.mypage.alarm.AlarmListApi
import kr.co.vividnext.sodalive.mypage.alarm.AlarmListRepository
import kr.co.vividnext.sodalive.mypage.alarm.AlarmListViewModel
import kr.co.vividnext.sodalive.mypage.auth.AuthApi
import kr.co.vividnext.sodalive.mypage.auth.AuthRepository
import kr.co.vividnext.sodalive.mypage.block.BlockMemberViewModel
import kr.co.vividnext.sodalive.mypage.can.CanApi
import kr.co.vividnext.sodalive.mypage.can.CanRepository
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeViewModel
import kr.co.vividnext.sodalive.mypage.can.charge.iap.CanChargeIapViewModel
import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanChargePgViewModel
import kr.co.vividnext.sodalive.mypage.can.coupon.CanCouponViewModel
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentTempRepository
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentTempViewModel
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentViewModel
import kr.co.vividnext.sodalive.mypage.can.payment.CanTempApi
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusViewModel
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateViewModel
import kr.co.vividnext.sodalive.mypage.profile.nickname.NicknameUpdateViewModel
@ -71,6 +104,7 @@ import kr.co.vividnext.sodalive.mypage.service_center.ServiceCenterViewModel
import kr.co.vividnext.sodalive.network.TokenAuthenticator
import kr.co.vividnext.sodalive.report.ReportApi
import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.settings.ContentSettingsViewModel
import kr.co.vividnext.sodalive.settings.SettingsViewModel
import kr.co.vividnext.sodalive.settings.event.EventApi
import kr.co.vividnext.sodalive.settings.event.EventRepository
@ -85,6 +119,7 @@ import kr.co.vividnext.sodalive.settings.terms.TermsRepository
import kr.co.vividnext.sodalive.settings.terms.TermsViewModel
import kr.co.vividnext.sodalive.user.UserApi
import kr.co.vividnext.sodalive.user.UserRepository
import kr.co.vividnext.sodalive.user.UserViewModel
import kr.co.vividnext.sodalive.user.find_password.FindPasswordViewModel
import kr.co.vividnext.sodalive.user.login.LoginViewModel
import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
@ -97,6 +132,7 @@ import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
class AppDI(private val context: Context, isDebugMode: Boolean) {
private val baseUrl = BuildConfig.BASE_URL
@ -119,6 +155,9 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
OkHttpClient().newBuilder()
.addInterceptor(logging)
.authenticator(TokenAuthenticator(get()))
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.build()
}
@ -131,12 +170,16 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
.build()
}
single { ApiBuilder().build(get(), AlarmListApi::class.java) }
single { ApiBuilder().build(get(), CanApi::class.java) }
single { ApiBuilder().build(get(), CanTempApi::class.java) }
single { ApiBuilder().build(get(), AuthApi::class.java) }
single { ApiBuilder().build(get(), UserApi::class.java) }
single { ApiBuilder().build(get(), MenuApi::class.java) }
single { ApiBuilder().build(get(), LiveApi::class.java) }
single { ApiBuilder().build(get(), TermsApi::class.java) }
single { ApiBuilder().build(get(), EventApi::class.java) }
single { ApiBuilder().build(get(), SeriesApi::class.java) }
single { ApiBuilder().build(get(), ReportApi::class.java) }
single { ApiBuilder().build(get(), LiveRecommendApi::class.java) }
single { ApiBuilder().build(get(), ExplorerApi::class.java) }
@ -145,6 +188,9 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
single { ApiBuilder().build(get(), AudioContentApi::class.java) }
single { ApiBuilder().build(get(), FaqApi::class.java) }
single { ApiBuilder().build(get(), MemberTagApi::class.java) }
single { ApiBuilder().build(get(), RouletteApi::class.java) }
single { ApiBuilder().build(get(), CreatorCommunityApi::class.java) }
single { ApiBuilder().build(get(), CategoryApi::class.java) }
}
private val viewModelModule = module {
@ -153,16 +199,17 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { TermsViewModel(get()) }
viewModel { FindPasswordViewModel(get()) }
viewModel { MainViewModel(get(), get(), get(), get()) }
viewModel { LiveViewModel(get(), get(), get()) }
viewModel { LiveViewModel(get(), get(), get(), get()) }
viewModel { MyPageViewModel(get(), get()) }
viewModel { CanStatusViewModel(get()) }
viewModel { CanChargeViewModel(get()) }
viewModel { CanChargePgViewModel(get()) }
viewModel { CanPaymentViewModel(get()) }
viewModel { CanPaymentTempViewModel(get()) }
viewModel { LiveRoomDetailViewModel(get()) }
viewModel { LiveRoomCreateViewModel(get()) }
viewModel { LiveTagViewModel(get()) }
viewModel { LiveRoomEditViewModel(get()) }
viewModel { LiveRoomViewModel(get(), get(), get()) }
viewModel { LiveRoomViewModel(get(), get(), get(), get()) }
viewModel { LiveRoomDonationMessageViewModel(get()) }
viewModel { ExplorerViewModel(get()) }
viewModel { UserProfileViewModel(get(), get(), get()) }
@ -172,23 +219,32 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { VoiceMessageViewModel(get()) }
viewModel { VoiceMessageWriteViewModel(get()) }
viewModel { SelectMessageRecipientViewModel(get(), get()) }
viewModel { SignOutViewModel(get()) }
viewModel { NoticeViewModel(get()) }
viewModel { EventViewModel(get()) }
viewModel { NotificationSettingsViewModel(get()) }
viewModel { ContentSettingsViewModel() }
viewModel { SettingsViewModel(get()) }
viewModel { SeriesDetailViewModel(get(), get()) }
viewModel { SeriesListAllViewModel(get()) }
viewModel { SeriesContentAllViewModel(get()) }
viewModel { SignOutViewModel(get()) }
viewModel { TextMessageDetailViewModel(get()) }
viewModel { LiveReservationStatusViewModel(get()) }
viewModel { AudioContentMainViewModel(get()) }
viewModel { AudioContentMainBannerViewModel(get()) }
viewModel { AudioContentMainRankingViewModel(get()) }
viewModel { AudioContentMainCurationViewModel(get()) }
viewModel { AudioContentMainOrderListViewModel(get()) }
viewModel { AudioContentMainNewContentViewModel(get()) }
viewModel { AudioContentMainRecommendSeriesViewModel(get()) }
viewModel { AudioContentViewModel(get()) }
viewModel { AudioContentOrderListViewModel(get()) }
viewModel { AudioContentUploadViewModel(get()) }
viewModel { AudioContentModifyViewModel(get()) }
viewModel { AudioContentThemeViewModel(get()) }
viewModel { AudioContentDetailViewModel(get(), get(), get(), get()) }
viewModel { AudioContentDetailViewModel(get(), get(), get(), get(), get()) }
viewModel { AudioContentCommentListViewModel(get()) }
viewModel { AudioContentCommentReplyViewModel(get()) }
viewModel { FollowingCreatorViewModel(get()) }
viewModel { FollowingCreatorViewModel(get(), get()) }
viewModel { ServiceCenterViewModel(get()) }
viewModel { ProfileUpdateViewModel(get()) }
viewModel { NicknameUpdateViewModel(get()) }
@ -196,29 +252,47 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { UserProfileDonationAllViewModel(get(), get()) }
viewModel { AudioContentCurationViewModel(get()) }
viewModel { AudioContentNewAllViewModel(get()) }
viewModel { AudioContentAllByThemeViewModel(get()) }
viewModel { AudioContentRankingAllViewModel(get()) }
viewModel { RouletteSettingsViewModel(get()) }
viewModel { CreatorCommunityAllViewModel(get(), get()) }
viewModel { CreatorCommunityCommentListViewModel(get()) }
viewModel { CreatorCommunityWriteViewModel(get()) }
viewModel { CreatorCommunityModifyViewModel(get()) }
viewModel { CanCouponViewModel(get()) }
viewModel { CanChargeIapViewModel(get()) }
viewModel { AlarmListViewModel(get()) }
viewModel { BlockMemberViewModel(get()) }
viewModel { UserViewModel(get(), get()) }
viewModel { MenuConfigViewModel(get()) }
}
private val repositoryModule = module {
factory { UserRepository(get()) }
factory { TermsRepository(get()) }
factory { LiveRepository(get(), get()) }
factory { SeriesRepository(get()) }
factory { LiveRepository(get(), get(), get()) }
factory { EventRepository(get()) }
factory { LiveRecommendRepository(get()) }
factory { AuthRepository(get()) }
factory { CanRepository(get()) }
factory { CanPaymentTempRepository(get()) }
factory { LiveTagRepository(get()) }
factory { ReportRepository(get()) }
factory { ExplorerRepository(get()) }
factory { MessageRepository(get()) }
factory { NoticeRepository(get()) }
factory { AudioContentRepository(get(), get()) }
factory { AudioContentRepository(get(), get(), get()) }
factory { AudioContentCommentRepository(get()) }
factory { PlaybackTrackingRepository(get()) }
factory { FollowingCreatorRepository(get(), get()) }
factory { FollowingCreatorRepository(get()) }
factory { FaqRepository(get()) }
factory { MemberTagRepository(get()) }
factory { UserProfileFantalkAllViewModel(get(), get()) }
factory { RouletteRepository(get()) }
factory { CreatorCommunityRepository(get()) }
factory { AlarmListRepository(get()) }
factory { MenuConfigRepository(get()) }
}
private val moduleList = listOf(

View File

@ -0,0 +1,174 @@
package kr.co.vividnext.sodalive.dialog
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.Window
import android.view.WindowManager
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.DialogMemberProfileBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.report.ProfileReportDialog
import kr.co.vividnext.sodalive.report.ReportType
import kr.co.vividnext.sodalive.report.UserReportDialog
import kr.co.vividnext.sodalive.settings.notification.MemberRole
import kr.co.vividnext.sodalive.user.UserViewModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class MemberProfileDialog(
private val activity: FragmentActivity,
private val layoutInflater: LayoutInflater,
private val memberId: Long,
private val screenWidth: Int
) : KoinComponent {
private val viewModel: UserViewModel by inject()
private val alertDialog: AlertDialog
val dialogView = DialogMemberProfileBinding.inflate(layoutInflater)
private lateinit var loadingDialog: LoadingDialog
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
setupView()
bindData()
}
fun dismiss() {
alertDialog.dismiss()
}
fun show() {
viewModel.getMemberProfile(memberId) {
alertDialog.show()
val lp = WindowManager.LayoutParams()
lp.copyFrom(alertDialog.window?.attributes)
lp.width = screenWidth - (40.dpToPx()).toInt()
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
alertDialog.window?.attributes = lp
}
}
private fun setupView() {
loadingDialog = LoadingDialog(activity, layoutInflater)
val profileLP = dialogView.ivProfile.layoutParams
profileLP.width = activity.resources.displayMetrics.widthPixels - (66.7f.dpToPx()).toInt()
profileLP.height = activity.resources.displayMetrics.widthPixels - (66.7f.dpToPx()).toInt()
dialogView.ivProfile.layoutParams = profileLP
dialogView.ivClose.setOnClickListener { dismiss() }
}
private fun bindData() {
viewModel.isLoading.observe(activity) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(activity) {
Toast.makeText(activity, it, Toast.LENGTH_SHORT).show()
}
viewModel.memberProfile.observe(activity) {
dialogView.ivProfile.load(it.profileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(8.dpToPx()))
}
dialogView.tvNickname.text = it.nickname
dialogView.tvBlockMember.text = if (it.isBlocked) {
"차단 해제"
} else {
"차단"
}
dialogView.tvBlockMember.setOnClickListener { _ ->
if (it.isBlocked) {
viewModel.memberUnBlock(it.memberId) { dismiss() }
} else {
showMemberBlockDialog(it.memberId, it.nickname)
}
}
dialogView.tvReportMember.setOnClickListener { _ ->
showMemberReportDialog(it.memberId)
}
dialogView.tvReportMemberProfile.setOnClickListener { _ ->
showProfileReportDialog(it.memberId)
}
}
}
private fun showMemberBlockDialog(memberId: Long, nickname: String) {
val message = if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
"""
${nickname}님을 차단하시겠습니까?
사용자를 차단하면 사용자는 아래 기능이 제한됩니다.
- 내가 개설한 라이브 입장 불가
- 나에게 메시지 보내기 불가
- 채널의 팬Talk 작성불가
""".trimIndent()
} else {
"""
${nickname}님을 차단하시겠습니까?
- 사용자를 차단하면 '차단한 사용자의 라이브 채팅' 보이지 않습니다.
""".trimIndent()
}
val dialog = android.app.AlertDialog.Builder(activity)
dialog.setTitle("사용자 차단")
dialog.setMessage(message)
dialog.setPositiveButton("차단") { _, _ ->
viewModel.memberBlock(memberId) { dismiss() }
}
dialog.setNegativeButton("취소") { _, _ -> }
dialog.show()
}
private fun showMemberReportDialog(memberId: Long) {
val dialog = UserReportDialog(activity, layoutInflater) {
viewModel.report(
type = ReportType.USER,
userId = memberId,
reason = it
) { dismiss() }
}
dialog.show(screenWidth)
}
private fun showProfileReportDialog(memberId: Long) {
val dialog = ProfileReportDialog(activity, layoutInflater) {
viewModel.report(
type = ReportType.PROFILE,
userId = memberId
) { dismiss() }
}
dialog.show(screenWidth)
}
}

View File

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

View File

@ -121,7 +121,11 @@ class ExplorerAdapter(
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
val item = items[position]
if (item.creators.isNotEmpty()) {
holder.bind(items[position])
}
}
override fun getItemCount() = items.size

View File

@ -1,11 +1,14 @@
package kr.co.vividnext.sodalive.explorer
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class GetExplorerResponse(
@SerializedName("sections") val sections: List<GetExplorerSectionResponse>
)
@Keep
data class GetExplorerSectionResponse(
@SerializedName("title") val title: String,
@SerializedName("coloredTitle") val coloredTitle: String?,
@ -14,6 +17,7 @@ data class GetExplorerSectionResponse(
@SerializedName("creators") val creators: List<GetExplorerSectionCreatorResponse>
)
@Keep
data class GetExplorerSectionCreatorResponse(
@SerializedName("id") val id: Long,
@SerializedName("nickname") val nickname: String,

View File

@ -0,0 +1,42 @@
package kr.co.vividnext.sodalive.explorer.profile
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
class CreatorFollowNotifyFragment(
private val onClickNotifyAll: () -> Unit,
private val onClickNotifyNone: () -> Unit,
private val onClickUnFollow: () -> Unit,
) : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(
R.layout.fragment_creator_follow_notify,
container,
false
)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<LinearLayout>(R.id.ll_notify_all).setOnClickListener {
onClickNotifyAll()
dialog?.dismiss()
}
view.findViewById<LinearLayout>(R.id.ll_notify_none).setOnClickListener {
onClickNotifyNone()
dialog?.dismiss()
}
view.findViewById<LinearLayout>(R.id.ll_unfollow).setOnClickListener {
onClickUnFollow()
dialog?.dismiss()
}
}
}

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