Compare commits

299 Commits

Author SHA1 Message Date
78c9b24bb9 feat(content-list-all): 정렬의 vertical padding을 12 -> 16으로 변경 2025-11-21 00:51:37 +09:00
6dc7c2578b feat(series-list-all): 시리즈 표시 어댑터를 최신 시리즈 표시 어댑터인 HomeSeriesAdapter로 변경 2025-11-21 00:23:56 +09:00
533da80986 feat(series-list-all): 완결시리즈 전체보기 페이지 추가 2025-11-20 18:26:49 +09:00
b7107e3069 feat(latest-audio-content-all): 테마 UI 변경, 아이템 2단으로 변경 2025-11-20 02:49:33 +09:00
bea9d8a709 feat(audio-content-all): 테마 UI 변경 2025-11-20 02:46:45 +09:00
1dc39cf786 feat(audio-content-all): 최신순/인기순 정렬 추가 2025-11-20 02:44:27 +09:00
a15b478ac6 feat(audio-content-all): theme 추가 2025-11-20 02:32:42 +09:00
5fa4c42119 build: versionCode 204, versionName 1.44.0 2025-11-19 18:08:32 +09:00
43a734bcc4 feat(series-main-by-genre): margin 간격 16 -> 24로 수정 2025-11-17 23:56:13 +09:00
5c4cb7a8f9 feat(live-room-agora): rtcEngine!!.setParameters("{\"che.audio.aiaec.working_mode\":0}") 추가하여 에뮬레이터에서 소리가 나가지 않던 버그 수정 2025-11-17 22:27:36 +09:00
cd8d2c255c feat(series-main): 추천시리즈, 요일별 시리즈 동일한 레이아웃을 사용하여 아이템 크기와 내용이 동일하게 표시되도록 수정 2025-11-17 21:32:03 +09:00
b759e110f8 feat(live-room): 왕하트 안내 메시지 표시 시간 3초 -> 5초로 수정 2025-11-17 18:03:37 +09:00
0dd2bcf07a feat(live-room): 왕하트 애니메이션 수정
- 수신자 가운데 하트 크기 sizeDp 고정에서 0 -> sizeDp까지 서서히 커지도록 수정
2025-11-17 17:16:09 +09:00
77e9c9eb5d feat(live-room): 왕하트 애니메이션 수정
- 하트 비의 하트 개수를 80~100개 랜덤으로 수정
2025-11-17 17:00:43 +09:00
bbb7858508 feat(live-room): 왕하트 애니메이션 수정
- 기존 가운데에서 한 번 폭발 후 비 내리는 애니메이션에서 가운데 + 랜덤 위치로 총 7번 폭발 후 비 내리는 애니메이션으로 수정
2025-11-17 16:57:27 +09:00
868b2d309a fix(home): fetchHome 후 불필요하게 콘텐츠 랭킹을 최신화하던 코드 제거 2025-11-17 16:03:13 +09:00
0cdc415a64 feat(chat-character): 작품별 탭 다시 추가 2025-11-13 23:04:41 +09:00
9b3d672e78 feat(chat-character): 캐릭터 신규 이미지 표시 UI 추가 2025-11-13 23:02:24 +09:00
0cfa5f8a32 feat(series-main): 시리즈 전체보기 장르별 탭 UI 및 데이터 2025-11-13 20:57:18 +09:00
907b718a3a feat(series-main): 시리즈 전체보기 페이지 추가
- 홈, 요일별, 장르별 탭 추가
- 홈 리스트 UI 및 데이터
- 요일별 UI 및 데이터
2025-11-13 18:27:04 +09:00
fba6d86018 fix(home): 추천 콘텐츠 섹션 제목 - 추천 콘텐츠로 고정 2025-11-12 20:32:50 +09:00
51b81f2ab6 feat(series-all): 오직 보이스온에서만(오리지널 시리즈) 전체보기 추가 2025-11-12 17:47:18 +09:00
ff16c70362 remove(series): 사용하지 않는 메서드 제거 2025-11-12 16:52:55 +09:00
f928fac9da fix(audio-content): 전체보기 페이지 UI/API 구현 2025-11-12 15:26:02 +09:00
a2262eff3f feat(home): 보온 주간 차트 콘텐츠 정렬 기준 추가
- 매출, 판매량, 댓글 수, 좋아요 수
2025-11-11 23:12:37 +09:00
62125f0873 feat(chat-character): 추천 캐릭터 섹션 추가 및 새로고침 API 반영 2025-11-11 17:17:18 +09:00
f97f9296b6 feat(chat-character): 큐레이션 영역 제거 2025-11-11 16:26:30 +09:00
3353ebb777 feat(home): 홈 추천 콘텐츠 섹션 추가 및 API 연동 2025-11-11 16:25:52 +09:00
81760ec99d fix: 사용하지 않는 이전 콘텐츠 메인 연관 파일 제거 2025-11-10 21:10:16 +09:00
27f0d01e81 fix(home): 사용하지 않는 큐레이션 영역 제거 2025-11-10 19:57:17 +09:00
1bf653a5d8 fix(home): 홈에 포인트 대여 콘텐츠 섹션 추가 및 데이터 연동
- 무료 콘텐츠 아래 동일 UI로 섹션 추가
- 제목 ‘포인트’ 컬러 강조(무료 섹션과 동일)
- GetHomeResponse.pointAvailableContentList 사용해 데이터 바인딩
- 섹션 우측 ‘전체보기’ 텍스트 추가(클릭 액션 TODO)
2025-11-10 19:38:48 +09:00
c35b267658 fix(login): 키보드 높이에 따라 화면을 위로 미는 로직이 BaseActivity, LoginActivity 두 군데에 있어서 2중으로 적용되는 버그 수정
- LoginActivity에 있는 키보드 높이에 따른 화면 Resize로직 제거
2025-11-10 18:51:21 +09:00
26f8d3dc45 version: versionCode 200, versionName 1.43.1 2025-11-06 18:35:31 +09:00
6620184fa0 temp(ai-chat): 작품별 탭 제거 2025-11-06 18:34:51 +09:00
1e8a96a52b fix(live-room): BIG_HEART_DONATION 메시지 heartMessage 3초, HEART_DONATION 1.5초 표시 적용
왜: BIG_HEART_DONATION 수신 시 heartMessage 표시 시간이 요구사항(3초)에 맞지 않았음. 무엇: heartMessage 표시 로직을 닉네임+표시시간 큐로 변경하고, HEART(1.5초)/BIG_HEART(3초)를 각 호출부에서 반영. 영향: 애니메이션 로직 변경 없음.
2025-11-06 17:35:08 +09:00
c0d998345d fix(live-room): Path로 그리는 하트 크기 133dp -> 200dp, 표시 시간 0.15초에서 0.3초로 수정 2025-11-06 17:11:49 +09:00
ed2258208b fix(live-room): 하트/캔 카운트 동시 업데이트 시 오차 수정
문제: LiveData.postValue 사용으로 연속 호출 시 병합(coalescing)으로 인해 로스트 업데이트가 발생하여 하트/캔 카운트 누락.
해결: ViewModel에서 메인 스레드 보장 후 setValue(value 할당)로 즉시 갱신하도록 변경. 비메인 스레드 호출 가능성에 대비해 mainHandler로 메인 재호출 처리.
영향: 빠르게 다수의 하트/캔 메시지가 도착해도 각 호출이 정확히 합산되며 오차 제거. 기존 서버 스냅샷 동기화(postValue)는 그대로 유지.
2025-11-06 16:37:17 +09:00
f4244d5913 fix(live-room): Path로 그리는 하트 모양 보정 2025-11-06 16:15:28 +09:00
b3a17b26dc perf(live-room): BIG_HEART 메시지 수신 경로를 Path 드로잉으로 전환하여 메모리 절감 2025-11-06 16:04:22 +09:00
a52f9425e8 fix(live-room): BIG_HEART 메시지 수신 되면 WaterWaveView 대신 임시 하트 뷰를 중앙에 표시 후 폭발 실행 2025-11-06 15:08:00 +09:00
48eb959ab2 fix(live-room): 잘못 사용 되어 효과가 없는 mutex 제거 2025-11-06 13:25:15 +09:00
0f30cf3880 fix(chat): IME 인셋 병합으로 키보드 표시 시 입력 영역 가림 문제 수정
- BaseActivity의 WindowInsets 리스너에서 systemBars와 ime 인셋의 각 방향별 최대값을 루트 패딩에 반영
- Edge-to-Edge 환경에서 하단 패딩이 키보드 높이만큼 확보되도록 개선
- ChatRoomActivity의 deprecated 설정 없이도 동작 유지
2025-11-05 11:56:36 +09:00
80431b7e83 refactor(live-room-like-heart): 하트 비의 하트와 폭발시 생기는 하트 파편을 동일한 모양으로 리팩토링 2025-11-05 01:07:03 +09:00
c4fc075844 feat(live-room-like-heart): 폭발 후 하트 비/우박 애니메이션 반영 2025-11-05 00:57:30 +09:00
a24b1a3b4e feature(live-room-like-heart): 롱프레스 왕하트 애니메이션 추가
- 물 채우기 애니메이션이 끝난 후 폭발 이펙트 추가
- 왕하트를 받은 크리에이터 및 다른 사람은 1초 동안 하트에 물이 채워지는 애니메이션이 수행된 후 폭발 이펙트가 실행된다.
2025-11-04 22:47:32 +09:00
601405349e feature(live-room-like-heart): 롱프레스 왕하트 애니메이션 변경
- 기존: 하트가 33.3dp 부터 커지는 애니메이션
- 변경: 하트가 133.3dp으로 고정되어 있고 물 채우기 애니메이션
2025-11-04 20:20:58 +09:00
332bf3256c fix(like-heart): 터치/클릭 충돌 해결 및 길이 기반 롱프레스 분기
- 1초 미만 터치 시 `handleHeartClick()` 실행되도록 수정
- 1초 경과 후에만 중앙 하트 표시 및 스케일 업데이트 시작
- ACTION_CANCEL 시 예약 러너블 취소, 중앙 하트 제거, 클릭/롱프레스 미실행
- 2초 이상 유지 시 기존 BIG HEART 트리거 로직 유지
- 가드 추가: `isLongPressBlockedByAvailability` 케이스 안전 처리
- 러너블/타이밍 추가: `showCenterHeartRunnable`, `longPressVisualStartTime`
2025-11-03 19:00:09 +09:00
6653ca2c11 feat(live-room): 하트를 길게(2초)간 누르면 표시 되는 왕하트(100캔) 추가, 애니메이션 제외 2025-11-03 16:23:44 +09:00
d6e9a63b1f feat(object-box): 사용하지 않는 object box 모델 삭제 2025-11-03 11:17:57 +09:00
5cc9f83a64 build(version): versionCode 199, versionName 1.43.1 2025-11-01 23:55:01 +09:00
da04cbcec0 feat(chat-작품별): 이미지 표시할 때 crossfade를 제거 2025-11-01 23:54:06 +09:00
1eff6702d7 feat(git): gitignore에 .kotlin/ 폴더는 git에서 관리하지 않도록 추가 2025-11-01 23:52:08 +09:00
6242c19397 feat(ai-chat): 임시로 제거했던 작품별 탭 다시 추가 2025-11-01 23:38:29 +09:00
194c4bad84 feature(agora): rtc version 4.5.2 2025-10-31 14:17:04 +09:00
1b7ba7825e feature(version): versionCode 198, versionName 1.43.0 2025-10-30 17:21:05 +09:00
5689dd10a5 feature(home): 지금 라이브 중인 라이브의 이미지를 크리에이터의 프로필 이미지가 표시되도록 수정 2025-10-30 17:02:29 +09:00
648064eac7 feature(version): versionCode 197, versionName 1.43.0 2025-10-30 16:01:10 +09:00
1ca6d068d0 live-room(agora): rtm version 1.5.3 -> 2.2.6 2025-10-30 14:54:21 +09:00
f08c481807 refactor(agora): 코드 파악을 좀 더 쉽게 할 수 있도록 코드 재배치 2025-10-27 23:07:44 +09:00
f64b28af1b feat(live-room): 사용하지 않는 후원현황 채팅 제거 2025-10-27 18:13:07 +09:00
2a50d0f5a0 build(live-room): agora rtc voice-sdk library version up
- voice-sdk:4.6.0
2025-10-24 01:19:39 +09:00
149d7358f0 build, fix(app): targetSdk 35 업그레이드 점검 및 Android 15 정확 알람 호환성 보완, Android 15 대응 보완
- 정확 알람 예외 처리 및 백그라운드 서비스 시작 회피
- setAlarmClock 호출부 SecurityException 처리 추가(1회/반복 알람)
- 401 응답 시 startService → stopService로 변경해 O+/15 백그라운드 서비스 제약 회피
2025-10-24 00:45:11 +09:00
a86e55eeae build(app): library upgrade
media3-session:1.8.0
media3-exoplayer:1.8.0
mockito-core:5.20.0
mockk:1.14.6
2025-10-24 00:28:21 +09:00
3979d37e76 build(app): library upgrade
firebase-bom:33.16.0
androidx.room:2.8.3
kotlinx-coroutines-android:1.10.2
af-android-sdk:6.17.4
2025-10-24 00:19:00 +09:00
d8d05b57cb build(app): library upgrade
media:1.7.1
core-ktx:1.16.0
appcompat:1.7.1
recyclerview:1.4.0
material:1.13.0
constraintlayout:2.2.1

webkit:1.14.0
lifecycle-livedata-ktx:2.9.4
lifecycle-viewmodel-ktx:2.9.4

gson:2.13.2
retrofit:3.0.0
converter-gson:3.0.0
adapter-rxjava3:3.0.0
logging-interceptor:5.2.1
tedpermission-normal:3.4.2
2025-10-24 00:02:40 +09:00
f1d718a45f build(app): bump compileSdk/targetSdk to 35
- compileSdk 35, targetSdk 35로 상향
- edge-to-edge를 적용하고 전체 화면에 insets를 추가 적용하여 이전과 동일하게 statusbar, navigationbar를 침범하지 않도록 처리
2025-10-23 23:32:58 +09:00
d33ab59378 fix(in-app-purchase): 인 앱 결제 완료 후 충전내역으로 이동하도록 코드 수정 2025-10-23 14:10:29 +09:00
f8e4a4fd45 build: versionCode 196, versionName 1.43.0 2025-10-23 14:09:39 +09:00
6d099e0aab build: versionCode 195, versionName 1.43.0 2025-10-23 12:06:11 +09:00
c5eb9767aa fix(iap): 인 앱 결제 라이브러리 버전 8.0.0 적용, 결제 보완사항 적용 — 즉시 소비, ITEM_ALREADY_OWNED 처리, obfuscatedAccountId 설정
- 구매 성공 직후 consume 처리하여 재구매 불가(ITEM_ALREADY_OWNED) 이슈 완화
- ITEM_ALREADY_OWNED 응답 시 미소비 구매 자동 정리 및 안내 메시지
- BillingFlowParams에 obfuscatedAccountId 설정으로 계정 연계 강화
- 서비스 연결 문제에 대한 사용자 메시지 보강
2025-10-22 23:40:14 +09:00
24672b7cf2 build(gradle): jvmTarget를 compilerOptions+jvmToolchain으로 마이그레이션
Kotlin 2.x에서 deprecated된 `kotlinOptions.jvmTarget` 사용을 제거하고
최신 DSL(`kotlin { jvmToolchain(17); compilerOptions { jvmTarget = JVM_17 } }`)로 전환했습니다.

왜: Kotlin Gradle Plugin 2.x에서 `kotlinOptions.jvmTarget`가 deprecated되어 빌드 경고가 발생했습니다.
무엇: `app/build.gradle`의 `kotlinOptions { jvmTarget = ... }` 제거 후, 최상위 `kotlin` 블록을 추가하여
- `jvmToolchain(17)` 설정
- `compilerOptions { jvmTarget.set(JVM_17) }` 적용
영향: 컴파일 타깃과 JDK 툴체인을 명시적으로 17로 고정하여 빌드 일관성을 확보하고 경고를 제거합니다.
2025-10-22 21:05:08 +09:00
db6de22273 사용하지 않는 databinding 설정 제거 2025-10-22 20:51:19 +09:00
8cdb82765f feat(build): agp 8.13.0, gradle-wrapper 8.14.3 업그레이드 2025-10-22 20:41:19 +09:00
172d7c0b80 feat(build): kotlin 2.2.20, agp 8.11.1 업그레이드 2025-10-22 20:08:40 +09:00
cf86dd3f30 fix(room): Kotlin 2.1/KSP 2.0 환경에서 KSP 오류 해결을 위해 Room 2.7.0으로 업그레이드
Kotlin을 2.1.21, KSP를 2.1.21-2.0.2로 올린 뒤 발생한
`unexpected jvm signature V` 예외를 해결하기 위해 Room(compiler, runtime, ktx, rxjava3)
버전을 2.6.1 → 2.7.0으로 업그레이드.

빌드가 정상 완료되며 KSP 태스크도 성공적으로 수행됨을 확인함.
2025-10-22 19:39:50 +09:00
23c05b91d5 build(room): KSP room.schemaLocation 설정 및 exportSchema=true로 스키마 export 활성화
프로젝트가 이미 KSP를 사용하고 있어 KSP 인수 기반으로 Room 스키마 export를 활성화했습니다.
- app/build.gradle: ksp { room.schemaLocation 등 } 추가
- Room DB 클래스 3종: exportSchema=true
- app/schemas 디렉터리 버전 관리
2025-10-22 19:23:58 +09:00
7ff3d7f1e5 refactor(root-gradle): deprecated 문법 신규 문법으로 전환
- task -> tasks.register로 전환
- rootProject.buildDir -> rootProject.layout.buildDirectory로 수정
2025-10-22 18:11:37 +09:00
912518c1ae refactor(config): buildConfig 설정 위치 권장 설정 위치로 변경
- 기존: gradle.properties android.defaults.buildfeatures.buildconfig=true

- 변경: build.gradle buildFeatures { buildConfig = true }
2025-10-22 16:33:46 +09:00
9b825ee244 refactor(db): ObjectBox 제거 및 Room으로 마이그레이션
- 최상위/app Gradle에서 ObjectBox 플러그인 제거
- PlaybackTracking을 Room Entity/DAO/Database로 전환
- Repository를 Room 기반으로 수정 및 Koin DI 주입 변경
2025-10-22 16:25:32 +09:00
bc581d763b fix(build): Room KAPT→KSP 마이그레이션 및 configuration cache 비활성화로 Kotlin 2.0 빌드 오류 해결
- Room을 2.6.1로 업데이트하고 KAPT를 KSP로 전환
- room-rxjava3 의존성 추가(RxJava3 반환 타입 지원)
- ObjectBox 플러그인과 충돌 회피를 위해 configuration cache 비활성화
- AGP 8.4.2 + Kotlin 2.0.21 환경에서 빌드 성공 확인
2025-10-22 13:50:42 +09:00
dd236d8f19 feat(live-reservation-all): 주간 캘린더 라이브러리 제거 및 개별 구현 2025-10-22 12:12:02 +09:00
ff236ee6a1 remove audio visualizer 2025-10-21 15:31:57 +09:00
66a6f992eb feat: versionCode 194, versionName: 1.42.1 2025-10-21 11:17:19 +09:00
c6438bef67 fix(home): 인기 캐릭터 -> 인기 캐릭터 채팅 2025-10-20 22:33:02 +09:00
ee5490939b fix(ChatRoom): 채팅 quota 구매 캔 개수 표시 수정
- 기존: 30결제하고 바로 대화 시작 -> 수정: 10(채팅 12개) 바로 대화 시작
2025-10-20 21:44:25 +09:00
65a2b47045 fix(GetHomeResponse): Character 클래스가 잘못 import 되어 있던 것 수정 2025-10-20 20:15:44 +09:00
a56c21f856 feat(user-profile): 팔로워 수 문구 팔로워 OO에서 팔로워 OO명으로 변경 2025-10-20 19:20:23 +09:00
7e501c794d feat(user-profile): 팬 Talk 답변 글 배경색 변경 2025-10-20 19:18:02 +09:00
c07fb33968 feat(user-profile): 더보기 버튼 흰색으로 변경 2025-10-20 18:58:14 +09:00
7ecb36a7be feat(home): 인기 캐릭터 색션 추가 2025-10-20 18:57:09 +09:00
1cec07f8c5 feat(user-profile): 팔로우/팔로잉 버튼 변경 2025-10-20 14:28:05 +09:00
ddcf191ade feat(user-profile): 최신콘텐츠 좋아요, 댓글 아이콘 크기 24x24 -> 18x18로 변경 2025-10-20 14:07:01 +09:00
945e3bd239 feat(temp): 작품별 탭 임시 제거 2025-10-17 14:44:49 +09:00
09ed73300d feat(user-channel): 팬 Talk 섹션 아이템 UI 수정 2025-10-17 09:22:41 +09:00
83fa3b870c feat(home): 인기 크리에이터 섹션 아이템 팔로우 버튼 표시 조건 추가
- 크리에이터 != 나 인 경우에만 팔로우/팔로잉 버튼 표시
2025-10-17 04:42:59 +09:00
cb67787925 feat(user-channel): 유저 채널 상단 툴바 오른쪽 상단 공유/메뉴 아이콘 정렬 수정
- LinearLayout으로 감쌈
- 메뉴 아이콘이 없어도 공유 아이콘이 오른쪽 상단에 위치할 수 있도록 정렬
2025-10-16 23:54:42 +09:00
ad053ef889 feat(user-channel): 유저 채널 라이브 아이템 터치 이벤트 추가 2025-10-16 23:52:25 +09:00
ae92921b7b feat(user-channel): 유저 채널 UI 수정
- 최신 콘텐츠 아이템 표시
- 후원 순위 아이템 사이즈 수정
- 섹션 제목 사이즈 업
2025-10-16 23:30:58 +09:00
9ba053b807 feat(user-channel): 유저 채널의 라이브 아이템 UI 수정 2025-10-16 19:00:46 +09:00
2b8b581082 feat(user-channel): 유저 채널의 프로필 이미지 사이즈와 섹션 순서 변경 2025-10-16 00:13:29 +09:00
0b775ed380 fix(payverse-webview): webView 세팅 조정을 통해 네이버페이가 동작하지 않던 버그 수정
- 참고: line 315 ~ 325
2025-10-15 15:39:34 +09:00
a90f4b1c5a fix(creator-community-write): 이미지를 선택하면 recordAudio영역이 보이도록 수정 2025-10-13 11:06:30 +09:00
5bc2b385fa feat(can): 사용 하지 않는 price 값 제거
feat(webview): payverse:// 스킴은 앱이 있으면 앱을 실행하도록 처리
2025-10-03 00:04:43 +09:00
21f57444c8 feat(can-payment): 다국적 통화 표기 지원 및 결제 금액 표시 개선
- KRW 고정 표기에서 벗어나 PG/해외 결제 등 다양한 통화 표기를 정확히 지원하기 위함
2025-10-02 17:14:49 +09:00
662f18bceb feat(can-charge): 이롬넷(Payverse) 통합결제 추가 2025-10-01 01:47:42 +09:00
2635b7d3c3 versionCode 191, versionName 1.42.1 2025-09-25 12:06:14 +09:00
aac3910b43 feat(original): 작품별 상세 UI
- 블러 처리한 배경의 세로 크기 절반으로 축소
2025-09-24 17:00:35 +09:00
0319981650 feat(original): UI 변경
- 캐릭터 / 작품 정보 탭 추가
- 작품 정보 탭 구성
  - 작품 소개
  - 원작 보러 가기
  - 상세 정보
    - 작가
    - 제작사
    - 원작
2025-09-19 18:35:18 +09:00
44e209d7b1 fix(ImagePickerCropper): openDocument 제거, excludeGif가 true이고 GIF 선택시 "GIF는 지원하지 않습니다." 메시지 반환 2025-09-18 22:02:54 +09:00
0f170c6daa fix(프로필 수정): gif 선택이 불가능 하도록 수정 2025-09-18 01:26:43 +09:00
67109bfe3c fix(Manifest): com.yalantis.ucrop.UCropActivity 추가 2025-09-18 01:02:18 +09:00
d22907c7d5 fix(이미지 선택): 이미지 선택 및 크롭 로직 수정 2025-09-18 00:17:20 +09:00
02155065f7 fix(liveroom-create): 경고 제거 2025-09-17 19:01:30 +09:00
3c21b36e88 fix: 라이브 생성 이미지 선택
- 이미지 선택 및 Crop 방법 변경
2025-09-17 18:49:36 +09:00
93fa042522 feat(character): 신규 캐릭터 전체보기 페이지 GRID
- 3단 구성에서 2단구성으로 변경
2025-09-17 02:45:45 +09:00
dcde2b125e feat(chat-original): 원작 상세 화면 및 캐릭터 무한 스크롤 로딩 구현 2025-09-15 19:19:00 +09:00
f15c6be1a4 feat(chat-original): ChatFragment에 작품별 탭 및 리스트 UI/API 연동 추가
- ChatFragment에 '작품별' 탭 추가 및 프래그먼트 스위칭 로직 반영
- /api/chat/original/list API, 모델, 레포지토리, ViewModel 추가
- OriginalTabFragment/Adapter/레이아웃 구현 (3단 그리드, 간격 16dp, 이미지 라운드 16dp, 아이템 이미지의 레이아웃 비율을 306:432)
- 스크롤 끝 감지를 구현하여 무한 스크롤을 지원
2025-09-15 16:21:54 +09:00
05208d3031 feat(chat-character): 신규 캐릭터 전체보기 화면 및 API 연동 추가 2025-09-13 02:15:01 +09:00
2b892fe783 feat(character): 본인인증 하지 않은 유저가 캐릭터 상세보기로 들어갈 때 본인인증 팝업 띄움 2025-09-12 01:13:08 +09:00
c3c19db730 feat(icon): 앱 아이콘 변경 2025-09-12 01:12:29 +09:00
b70c8058e8 feat(splash): 스플래시 페이지 수정 2025-09-11 22:16:00 +09:00
cdc59d0877 fix(main): 라이브 탭 <-> 채팅 탭 순서 변경 2025-09-11 20:06:16 +09:00
88d13ce77a fix(character): 인기 캐릭터
- TextView 숫자 하단 여백(descent) 제거
2025-09-11 20:04:13 +09:00
f830c98b8e fix(character-detail): 캐릭터 정보
- 캐릭터 이름과 MBTI 사이 간격 8로 수정
2025-09-11 14:58:30 +09:00
8de0dc2242 feat(chat): Talk 탭에 RecyclerView 스크롤 페이지네이션 추가
- /api/chat/room/list 호출에 page 파라미터 적용 (0부터 시작)
- ViewModel에 currentPage/lastPageReached 상태 추가 및 append 로직 구현
- Fragment에 스크롤 리스너로 바닥 근접 시 다음 페이지 자동 로드
- 빈 데이터 시 마지막 페이지로 간주하여 추가 로딩 중단
2025-09-11 14:38:29 +09:00
56e99912d4 "fix(chat-room): 쿼터 UI를 totalRemaining 대신 nextRechargeAtEpoch 기준으로 갱신 2025-09-10 13:51:07 +09:00
9ed3c046b3 fix(chat-room): 채팅방
- 쿼터 상태 조회, 쿼터 구매 API URL 변경
2025-09-10 12:03:49 +09:00
65791c55ca feat(ui): enforce 2:3 aspect ratio and center chatroom background
- item_character_gallery.xml: set iv_image to 2:3
- activity_chat_room.xml: apply H,2:3 ratio and center frame by constraining top/bottom to parent
- item_chat_background_image.xml: set picker item to 2:3
- align dim view constraints to match background area
2025-09-05 18:30:58 +09:00
0422746267 fix(chat-room settings): 배경 사진 -> 배경 이미지 로 변경 2025-09-05 18:27:22 +09:00
cc3aca34f5 fix(character-detail): 캐릭터 정보 추가
- mbti, 나이, 성별 추가
2025-09-05 17:43:04 +09:00
e39bdb6b03 fix(character-detail): 상단 툴바 제목을 "캐릭터 정보"로 고정 2025-09-05 14:16:25 +09:00
27a36d2d44 fix: place_holder 변경 2025-09-05 12:52:33 +09:00
60b7bb7e7e fix(character): 캐릭터 이미지 RoundedCorner 16dp 적용 2025-09-05 12:48:26 +09:00
8ebaaefd6f fix(character-main): 큐레이션 섹션 데이터 이름 수정
- CurationSection.kt
- id -> characterCurationId
2025-08-29 14:47:05 +09:00
201ab488b2 fix(character-main): 최근 대화 캐릭터
- 터치시 채팅방이 아닌 캐릭터 상세 페이지로 이동
2025-08-28 20:00:22 +09:00
8b241709e1 fix(chat): 대화 설정
- 대화 초기화 오른쪽에 30캔 안내 추가
2025-08-28 01:53:45 +09:00
d9cb12e882 fix(chat): 채팅방 입장 시 서버 멤버 정보로 캔 배지 동기화
- ChatRoomActivity에서 getMemberInfo 호출 추가
- 응답 성공 시 SharedPreferenceManager.can/point 갱신 및 헤더 배지 즉시 반영
- 네트워크 실패 시 UI 흐름 방해 없이 조용히 무시 처리
2025-08-28 00:46:52 +09:00
5c78c567ca fix(chat): 대화 초기화 성공 시 로컬 데이터 삭제 및 로딩 다이얼로그 적용
- ChatMessageDao: deleteMessagesByRoomId(roomId) 추가
- ChatRepository: clearMessagesByRoom(roomId) 추가
- ChatRoomActivity:
  - clearLocalPrefsForRoom(roomId) 구현
  - reset 플로우에 Prefs/DB 삭제 체인 연결
  - onResetChatRequested()에서 LoadingDialog 표시 및 doFinally로 닫힘 보장
2025-08-28 00:23:14 +09:00
e3bcc6d3a6 사용하지 않는 함수 삭제 2025-08-27 16:50:54 +09:00
05e8874d81 fix(chat): 대화 초기화 성공 시 방별 로컬 데이터(배경/공지/메시지) 삭제 처리
- ChatMessageDao: deleteMessagesByRoomId(roomId) 추가
- ChatRepository: clearMessagesByRoom(roomId) 추가
- ChatRoomActivity: clearLocalPrefsForRoom(roomId) 구현 및 reset 플로우에 Prefs/DB 삭제 체인 연결
- 요구사항: 대화 초기화 API 성공 시 해당 방의 배경 데이터와 로컬 메시지 등 모든 관련 데이터 제거
2025-08-27 16:49:27 +09:00
88e3ae7b51 fix(chat): 배경 선택 다이얼로그에서 초기 선택 복원이 되지 않는 문제 수정
- 선택 상태를 URL 비교에서 이미지 ID 우선 방식으로 변경
- URL만 저장된 기존 데이터에 대해 목록 로드 후 URL→ID 마이그레이션 추가
- SharedPreferences에 chat_bg_image_id_room_{roomId} 키 도입(호환 위해 URL 키 유지)
2025-08-27 15:53:43 +09:00
02df0b6774 feat(chat): 메시지 괄호 지문 색상을 회색으로 변경 2025-08-27 14:10:30 +09:00
a941d0bfab feat(chat): 채팅방 배경 사진 변경 기능 추가
- ChatRoomMoreDialog에서 배경 사진 변경 Picker 연결
- my-list API 추가 및 Repository 위임 추가
- 배경 선택 Dialog(3열 Grid, 4:5 비율) 및 선택 상태 UI 구현
- SharedPreferences로 roomId별 배경 URL 저장/로드
- ChatRoomActivity에 배경 저장/적용 헬퍼 추가 및 기본 프로필 적용 로직 구현
2025-08-27 02:37:20 +09:00
2e837bec5d feat(chat-quota): 쿼터 연동 및 카운트 다운 / 쿼터 구매 UX 개선(+5초 표시 보정)
- TalkApi: /api/chat/quota/me, /api/chat/quota/purchase 엔드포인트 추가
- Repository: getChatQuotaStatus(), purchaseChatQuota() 추가, sendMessage 응답 타입을 SendChatMessageResponse로 전환
- Model: ChatQuotaStatusResponse/ChatQuotaPurchaseRequest 추가, SendChatMessageResponse/ChatRoomEnterResponse 기본값 추가
- UI(Adapter): QuotaNotice 뷰타입/레이아웃 추가, 안정 ID/부분 갱신(payload) 적용, Change 애니메이션 비활성화로 깜빡임 최소화
- UI(Activity): 쿼터 0 시 입력창 숨김 + 안내 노출, 00:00:00 도달 시 /quota/me 조회
- 카운트다운 계산: epoch 기반 남은 시간 계산 + 표시용 +5초(DISPLAY_FUDGE_MS) 가산
- 구매 성공 시 로컬 30캔 차감 및 헤더 배지 즉시 갱신
2025-08-26 21:36:31 +09:00
9b1a83bd69 feat(chat-room): 대화 설정 다이얼로그 구현 및 채팅방 초기화 API 연동
- MoreDialog UI 구성 및 동작(배경 스위치/변경, 대화 초기화, 신고하기)
- 방별 배경 표시 SharedPreferences 저장 및 화면 반영
- TalkApi에 resetChatRoom 엔드포인트 추가, Repository 메서드 추가
- ChatRoomActivity와 다이얼로그 연동, 초기화 플로우 구현
2025-08-26 13:37:58 +09:00
b3553f80c6 feat(chat): 채팅방 상단 캔 배지 및 더보기 전체화면 다이얼로그 추가
- 헤더 우측에 캔 배지(tv_can_badge)와 더보기(iv_more) 추가
- 캔 배지 스타일 적용(배경 #263238, 텍스트 white, v5/h8 패딩, can 아이콘)
- 더보기 클릭 시 전체화면 다이얼로그 표시(플레이스홀더 UI)
2025-08-26 12:11:43 +09:00
5d76ff1590 feat(chat): AI 유료/이미지 메시지 및 구매 플로우 추가
- ServerChatMessage/ChatMessage에 messageType/imageUrl/price/hasAccess 필드 반영
- TalkApi/Repository: 유료 메시지 구매 API 연동 및 성공 시 로컬 DB 반영
- ChatRoomActivity: 구매 팝업 SodaDialog 적용(취소/잠금해제) 및 구매 성공 시 메시지 교체
- ChatMessageAdapter: 이미지 렌더링(라운드 10dp), 유료 오버레이(가격+"눌러서 잠금해제") 처리,
  구매/캐러셀 오픈 콜백 추가
- 구매된 이미지 클릭 시 전체화면 캐러셀 지원
- item_chat_ai_message.xml: 메시지 UI 최대 90% 폭, 시간 텍스트 배치 개선, 이미지 4:5 비율 적용
- 그룹 메시지 간 간격 절반 적용(ItemDecoration)
- Room DB v2 마이그레이션: messageType/imageUrl/price/hasAccess 컬럼 추가로 재입장 시 표시 문제 해결

왜:
- 유료/이미지 메시지 기능 제공 및 일관된 구매 경험 필요
- 재입장 시 이미지/유료 정보 누락 문제(DB 정합) 해결
- 시간 잘림/배치 문제와 그룹 간격 시인성 개선
2025-08-25 17:22:56 +09:00
6c57c5a98a feat(character-gallery): 구매 이미지 전체화면 Carousel 뷰어 추가
구매된 이미지를 탭하면 전체화면 DialogFragment로 열리고,
ViewPager2 기반 Carousel로 좌우 슬라이딩 탐색이 가능하도록 구현.
2025-08-23 01:48:57 +09:00
770c4179a3 fix(gallery): 구매 다이얼로그를 AlertDialog에서 SodaDialog로 교체
디자인 일관성 및 공통 컴포넌트 적용을 위해 갤러리 탭의 구매 확인 다이얼로그에
SodaDialog를 사용하도록 변경
2025-08-22 22:17:31 +09:00
9164942395 feat(gallery): 로딩 다이얼로그 표시 및 이미지 캐싱 적용
Fragment에서 isLoading에 따라 Loading Dialog를 표시/해제.
Glide에 디스크 캐싱 적용으로 스크롤 성능 개선.
2025-08-22 22:12:36 +09:00
e3ed816fb3 feat(gallery): 캐릭터 이미지 구매 기능 추가
갤러리 아이템의 구매 버튼 클릭 시 확인 다이얼로그를 표시하고,
확인 시 /api/chat/character/image/purchase API를 호출하여 이미지 URL을 갱신.
구매 성공 시 isOwned=true 처리 및 보유 비율/개수 업데이트.
2025-08-22 21:49:44 +09:00
13ee098cfc feat(character-gallery): 갤러리 탭 UI/페이징 및 API 연동, DI 적용
- API: CharacterApi에 이미지 리스트 API 추가(characterId, page, size)
- VM: 페이징(loadInitial/loadNext), 요청 중복 방지, 마지막 페이지 판단, 누적 리스트 관리
- UI: ProgressBar(배경 #37474F/진행 #3BB9F1, radius 999dp, 비활성) + 좌/우 텍스트 구성
- Grid 3열 + 2dp 간격, item 4:5 비율, 잠금/구매 버튼 UI 적용
- UX: tv_ratio_right에서 ownedCount만 #FDD453로 강조(white 대비)
2025-08-22 17:03:01 +09:00
f917eb8c93 fix(character-detail): characterId 전달 및 상세 탭 전환 로직 수정
fix(character-detail): 탭 전환 시 프래그먼트 캐싱하여 재로딩 방지

CharacterDetailFragment에 newInstance(characterId) 도입 및 ARG 전달 구조 추가.
Fragment에서 잘못된 intent 참조 제거하고 arguments → activity.intent 순으로 안전하게 조회.
Activity 초기 진입 시 상세 탭 로딩 경로 정리 및 characterId 유효성 검사 시 종료 처리 보강.

replace 기반 교체를 add/show/hide 구조로 전환.
TAG_DETAIL/TAG_GALLERY로 인스턴스를 식별하여 FragmentManager 복원/재사용.
탭 이동 시 기존 인스턴스 표시만 수행하여 onViewCreated 재호출/네트워크 재요청 방지.
2025-08-22 15:23:17 +09:00
989a0f361b feat(character-detail): 캐릭터 상세
- 탭 UI 추가
2025-08-22 03:39:36 +09:00
52c1f61109 feat(report): 캐릭터 댓글 신고 사유를 라디오 버튼으로 변경 및 비활성 시각화
- 댓글 신고 사유 리스트 변경
- 댓글 신고 사유 선택 UI를 RadioGroup/RadioButton으로 전환
- 선택 전 신고 버튼 비활성화 및 alpha 적용으로 시각적 비활성화 처리
- 선택 시 버튼 활성화 및 alpha 복구
2025-08-22 03:04:50 +09:00
7dd6d46a5f fix(talk-tab): 채팅방 리스트
- 채팅방 사이 간격 24
- 이미지 원형으로 변경
2025-08-22 02:34:05 +09:00
3a1943ba87 refactor(character-comment): 캐릭터 댓글/답글 리스트
- 배경색 변경
- 댓글 사이 간격 조정
2025-08-20 18:38:16 +09:00
ab1dd04a60 refactor(character-comment): 답글 리스트 MVVM 적용 및 ViewModel 추가
- CharacterCommentReplyViewModel 추가: 로딩/토스트/페이지네이션/CRUD 로직 이관
- AppDI Koin 모듈에 Reply ViewModel 등록
- CharacterCommentReplyFragment에서 Repository 직접 접근 제거 및 바인딩 로직 추가
2025-08-20 16:49:51 +09:00
ccd88dad47 refactor(chat/character): 댓글 리스트 화면에 ViewModel 도입 및 Fragment-Repository 직접 의존 제거
CharacterCommentListViewModel을 추가하여 댓글 조회/등록/삭제/신고 및 페이지네이션 로직을 ViewModel로 이전.
Fragment는 UI 업데이트와 사용자 입력 처리에 집중하도록 리팩토링.
Koin DI에 ViewModel 등록.
2025-08-20 16:22:34 +09:00
fdc9ba80e0 fix(comment): 답글 더보기 Bottom Sheet 적용 및 삭제/신고 API 연동
답글 리스트에서 PopupMenu를 Bottom Sheet로 통일하고, 내 답글은 삭제, 타인 답글은 신고 메뉴만 노출하도록 변경.
삭제는 원 댓글 삭제와 동일한 API(deleteComment)를 사용하며, 신고는 reportComment로 연동.
2025-08-20 15:55:59 +09:00
d1c62fd2b6 fix(comment): 캐릭터 댓글 신고 BottomSheet가 표시되지 않는 문제 수정
- childFragmentManager 대신 parentFragmentManager로 신고 BottomSheet 표시
- BottomSheet dismiss 직후 show 트랜잭션 충돌/우선순위 이슈 완화
2025-08-20 15:33:47 +09:00
3e2cdd502c fix(character-comment): 캐릭터 댓글 수 표시 수정
- 서버에서 받아온 댓글 수를 표시하도록 수정
2025-08-20 14:00:46 +09:00
c78aed2551 fix(comment): 캐릭터 댓글 더보기에서 삭제 API 연동 및 UI 반영
- Bottom Sheet 삭제 선택 시 deleteComment API 호출 추가
- 성공 시 목록에서 항목 제거
- 오류 시 사용자에게 에러 토스트 노출
2025-08-20 03:19:23 +09:00
e881178f2a feat(character-comment): 답글 리스트 API 연동 및 커서 기반 무한 스크롤 적용
feat(character-comment): 답글 작성 API 연동 및 성공 시 낙관적 UI 반영

- CharacterCommentReplyFragment에 listReplies API 연동
- 초기 1회 로드 허용, 이후 cursor != null일 때만 추가 로드
- isLoading 플래그로 중복 요청 방지
- 어댑터 헤더(원본 댓글) 유지, replies만 순차 추가

- CharacterCommentReplyFragment에서 createReply API 호출로 스텁 제거
- 요청 중 로딩 다이얼로그 표시, 성공 시 입력 초기화 및 리스트에 즉시 추가
- 에러 처리(토스트) 적용
2025-08-20 03:07:35 +09:00
b995a0b151 feat(character-comment): 답글 리스트 API 연동 및 커서 기반 무한 스크롤 적용
- CharacterCommentReplyFragment에 listReplies API 연동
- 초기 1회 로드 허용, 이후 cursor != null일 때만 추가 로드
- isLoading 플래그로 중복 요청 방지
- 어댑터 헤더(원본 댓글) 유지, replies만 순차 추가
2025-08-20 02:48:01 +09:00
ec315c4747 feat(character-comment): 캐릭터 댓글 리스트 등록/목록/신고 API 연동 및 DI 등록
fix(character-comment): 캐릭터 댓글 리스트 무한 스크롤에서 cursor null 시 추가 호출 방지

- CharacterCommentApi/Repository 추가
- AppDI에 API/Repository 등록
- CharacterCommentListFragment: 등록 버튼 클릭 시 API 호출로 전환, 커서 페이징 목록 로드 적용, 신고 API 연동
- 로딩/에러 처리 및 중복 로드 방지 플래그 추가

- 스크롤 리스너에 canLoadMore 조건 추가(초기 또는 cursor 존재 시에만 호출)
- loadMore()에 종료 가드 추가(adapter 비어있지 않고 cursor null이면 반환)
- 댓글 1개인 경우 동일 내용 반복 로딩 문제 해결
2025-08-20 02:37:14 +09:00
52ff0c82cb feat(character-comment): 신고 BottomSheet 추가 및 삭제 확인 팝업 도입
- 신고 BottomSheet(제목/단일선택 리스트/신고 버튼) 구현 및 더보기→신고 흐름 연동
- 삭제 버튼 클릭 시 확인 다이얼로그 표시 후 확정 시 리스트에서 제거
- 신고/삭제 API 호출부는 스텁으로 남겨둠(후속 연동 예정)
2025-08-20 01:22:56 +09:00
d4ec2fbdef feat(character-comment): 답글 페이지 UI 및 페이징 스텁 구현
- 댓글 리스트 아이템 터치 시 답글 페이지로 전환 연결
- 상단 뒤로 가기/닫기, 입력 폼, divider, 원본 댓글, 들여 쓰기된 답글 목록 구성
- RecyclerView 최하단 도달 시 더미 데이터 추가 로드(무한 스크롤 스텁)
- 답글 등록/수정/삭제 동작 스텁 처리
- 추가 파일
  - layout: fragment_character_comment_reply.xml, item_character_comment_reply.xml
  - 코드: CharacterCommentReplyFragment, CharacterCommentReplyAdapter
- 변경 파일
  - CharacterCommentListBottomSheet: openReply() 추가
  - CharacterCommentListFragment: 아이템 클릭 시 답글 페이지 진입
2025-08-20 00:54:00 +09:00
a9742a07c0 feat(character-comment): 캐릭터 댓글 리스트 BottomSheet UI 및 페이징 스텁 구현
- CharacterDetail 댓글 섹션 터치 시 BottomSheet 표시
- 헤더/입력폼/Divider/리스트/더보기 BottomSheet 구성
- RecyclerView 하단 도달 시 더미 데이터 추가 로드(Stub)
- 상대시간 표기(분/시간/일/년 전)
- API 연동은 이후 작업 예정 (스텁)
2025-08-20 00:42:15 +09:00
df1746976c feat(character-detail): 캐릭터 상세 댓글 섹션 추가 및 데이터 바인딩
- 댓글 입력 필드 stroke(흰색 1dp stroke와 radius 5dp) 추가
- 입력 박스 내부 우측에 전송 아이콘(ic_message_send) 추가
- 배경 드로어블(#263238, radius 10dp) 추가
- CharacterCommentResponse에 comment(nullable) 필드 추가
- CharacterDetailActivity에서 latestComment/totalComments 바인딩 및 UI 분기 처리
2025-08-19 18:37:12 +09:00
61cfbe249c fix(character-detail): 더보기 버튼 미표시 문제 수정 (줄 수 측정 시점 조정)
세계관/성격 텍스트의 줄 수를 maxLines=3 적용 이전에 측정하도록 순서 변경.
측정 후 더보기 가시성 결정, 그 다음 접힘 레이아웃 적용.
확장 상태 플래그 및 아이콘/문구 초기화 추가.
2025-08-18 19:13:26 +09:00
f9b50089dd fix(chat): 캐릭터 상세
- 세계관 -> [세계관 및 작품 소개]
- 성격 -> [성격 및 특징]
- 전체보기 -> 더보기
2025-08-18 16:37:29 +09:00
95983dcf5b fix(chat): 최근 대화한 캐릭터
- 캐릭터 이미지 원형으로 변경
2025-08-18 16:31:46 +09:00
16e8941c15 fix(chat): 캐릭터 상세
- 캐릭터 이미지 딤 제거
- 캐릭터 정보: 이미지 아래로 이동
2025-08-15 01:03:43 +09:00
cd4a098bff fix(chat): 동시간대 메시지 정렬을 messageId 오름차순으로 안정화
createdAt만 사용하던 정렬 로직을 다중 키로 변경하여
동일 시간에 messageId 오름차순이 보장되도록 수정.
- 로컬 초기 로드: createdAt -> messageId -> localId asc
- 서버 초기/증분 로드: createdAt -> messageId asc
2025-08-15 00:45:22 +09:00
4a0940ad26 fix(chat-room): 프로필 이미지 circle로 변경 2025-08-15 00:37:08 +09:00
dd7251f18b fix(chat-room): 채팅 아이템 UI, 메시지 입력 창 UI
- 채팅 아이템이 화면을 벗어나는 버그 수정
- 메시지 입력창 글자크기 14sp, rounded corner 32dp
2025-08-15 00:29:56 +09:00
3d727f07fa fix(chat-room): header_container
- 이름과 캐릭터 타입을 세로로 표시
2025-08-14 23:01:31 +09:00
92883ee577 fix(chat-room): 메시지 전송 API URL 수정
기존
/api/chat/room/{roomId}/messages

변경
/api/chat/room/{roomId}/send
2025-08-14 22:40:27 +09:00
2790bea1d8 fix(chat-room): stable IDs 설정 시점을 setAdapter 이전으로 이동
- ChatMessageAdapter: onAttachedToRecyclerView에서 setHasStableIds 호출 제거
- ChatRoomActivity: 어댑터 생성 직후 setHasStableIds(true) 설정 후 RecyclerView에 연결

원인: 옵저버 등록 이후 setHasStableIds 변경으로 런타임 예외 발생
검증: 단위 테스트 모두 통과, 빌드 성공
2025-08-14 22:36:50 +09:00
3f87b35816 refactor(chat-room): 페이징 커서 fallback/저장 로직을 createdAt→messageId로 정합성 수정
- 왜: 서버 계약에 따라 cursor 의미가 단독 messageId로 확정됨. createdAt 기반 커서는 페이징 경계에서 중복/누락을 유발할 수 있음
- 무엇: ChatRoomActivity.loadMoreMessages()/loadInitialMessages()에서 cursor 계산 및 nextCursor 대체 저장을 messageId 기준으로 변경. Repository/API 타입은 그대로 유지
2025-08-14 21:27:17 +09:00
bd86d1610a fix(chat-room): api url 수정
- /api/chat/rooms/... -> /api/chat/room/...
2025-08-14 20:29:36 +09:00
7f1b1b1ed3 feat(chat-room): 안내 메시지 접힘 상태 저장시 사용하는 key
- string 오류로 인해 제대로 표시 되지 않던 버그 수정
2025-08-14 20:25:35 +09:00
09b8979ba0 feat(chat-room): sendMessage 응답 다건 변경 반영
- TalkApi.sendMessage: ApiResponse<List<ServerChatMessage>>로 변경
  - ChatRepository.sendMessage: Single<List<ServerChatMessage>>로 변경. 로컬 SENDING→SENT 업데이트 후, 응답 메시지 전체를 DB에 저장
  - ChatRoomActivity: 구독부에서 List를 처리하며 mine == false(AI) 메시지들만 순서대로 append. 타이핑 인디케이터는 성공/실패 시 동일하게 제거
2025-08-14 20:23:21 +09:00
02747c539b test(chat-room): 타이핑 인디케이터 표시/중복/숨김 테스트 추가
- showTypingIndicator 중복 호출 시 중복 삽입 방지 검증
- hideTypingIndicator 안전성 검증(표시되지 않은 경우도 안전)
- NPE 회귀 방지

fix(adapter): RecyclerView 미부착 상태에서 notify 호출로 NPE 발생 방지
2025-08-14 19:19:38 +09:00
c1012586ce fix(chat-room): 접근성 라벨 및 다국어 문자열 적용
- 레이아웃 contentDescription 하드코딩 제거 및 strings 리소스화
- ChatMessageAdapter 접근성 문구를 리소스 기반으로 변환
- values-en 추가로 안내/버튼/접근성/상태 문구 영문화
- 타이핑 인디케이터 접근성 라벨 추가
2025-08-14 18:50:32 +09:00
c9b6623eac perf(chat): DiffUtil 및 stableIds 적용으로 채팅 리스트 갱신 최적화
- ChatMessageAdapter에 DiffUtil 기반 submitList 도입으로 불필요한 전체 바인딩 제거
- RecyclerView 연결 시점에만 stableIds 활성화하여 테스트 환경 NPE 회피
- AI 프로필 이미지 중복 로딩 방지(tag 비교)로 네트워크/디코딩 비용 절감
- onViewRecycled에서 애니메이션/리스너/이미지 정리로 메모리 안정성 향상
2025-08-14 18:13:40 +09:00
d662bd0b65 feat(chat-ui): 메시지 그룹화, 시간 포맷팅, Repository 테스트 추가 2025-08-14 18:08:01 +09:00
ec60d4f143 fix(settings): 로그아웃 시 로컬 채팅 메시지 전체 삭제 연동
- SettingsViewModel에 ChatRepository 주입 및 삭제 로직 처리
- DI(Koin) 수정으로 SettingsViewModel에 ChatRepository 바인딩
- 삭제 실패 시에도 사용자 로그아웃 흐름 유지
2025-08-14 17:30:44 +09:00
373752f592 add(gitignore): .idea/deviceManager.xml 추가 2025-08-14 17:15:50 +09:00
933e650183 feat(chat-room): 채팅 API 연동 및 전송/페이징 플로우 구현 완료
- TalkApi에 입장/전송/점진 로딩 엔드포인트 구현(9.1)
- ChatRepository를 통한 서버 연동 및 로컬 동기화 추가
- ChatRoomActivity에서 입장/전송/페이징 연동, 타이핑 인디케이터/에러 처리 반영(9.2)
2025-08-14 17:14:43 +09:00
6a6aa271ef feat(chat): 톡 목록 스키마 반영 및 채팅방 진입 연결
- TalkRoom 필드 변경 및 신규 스키마 적용
- 어댑터 바인딩/DiffUtil 수정, 프로필 이미지 28dp 라운드 처리
- 아이템 클릭 시 ChatRoomActivity로 이동(roomId 전달)
- item_talk 배경 제거, 최근 캐릭터 썸네일 모서리 28dp로 통일
2025-08-14 14:46:12 +09:00
012437e599 feat(character-main): 최근 대화한 캐릭터
- 이미지 표시 및 클릭 이벤트 적용
2025-08-14 01:04:53 +09:00
d3a64d8359 feat(chat-room): Coil 기반 프로필 이미지 로딩 유틸 도입 및 적용
채팅방의 프로필 이미지 로딩을 공용 유틸(loadProfileImage)로 통일하고
플레이스홀더/에러 처리 및 둥근 모서리 변환을 기본 적용했습니다.

- ImageLoader.kt 추가: loadProfileImage(ImageView, url, cornerRadiusDp)
- ChatMessageAdapter: AI 프로필 이미지 로딩에 유틸 적용
- ChatRoomActivity: 헤더 프로필 이미지 로딩에 유틸 적용 (배경 이미지는 기존 유지)
2025-08-14 00:05:18 +09:00
7451fccff9 feat(chat-room): 시간 포맷팅 유틸 formatMessageTime 도입 및 어댑터 리팩토링
UTC timestamp를 로컬 타임존/로케일 기준 "오전/오후 h:mm" 형식으로 변환하는
공용 유틸(TimeUtils.kt)을 추가하고, ChatMessageAdapter에서 기존 파일 레벨
함수를 제거하여 공용 유틸을 사용하도록 리팩토링했습니다.

- TimeUtils.kt 추가: formatMessageTime(timestamp: Long, locale: Locale)
- ChatMessageAdapter: private 함수 제거 및 import 정리
2025-08-13 23:57:39 +09:00
1882139fac feat(chat-room): 7.1 로컬 우선 표시 및 오프라인 대체 처리 추가
- 진입 시 로컬 최근 20개 메시지 즉시 표시
- enterChatRoom 응답으로 최신 상태로 전체 갱신
- 네트워크 실패 시 로컬 UI 유지 및 토스트 노출
2025-08-13 23:46:45 +09:00
7fc72da905 feat(chat-room): 7.3 로컬 DB 동기화 및 메시지 상태/정리 로직 구현
- ChatMessageDao에 상태 업데이트/정리 보조 쿼리 추가
- ChatRepository에 로컬 저장, 상태 업데이트, 오래된 메시지 정리 API 추가
- Activity 전송/상태 변경 시 DB 반영 및 로딩 후 정리 트리거
2025-08-13 23:36:50 +09:00
9fa270da10 feat(chat-room): 7.2 점진적 메시지 로딩 구현 및 중복 방지 처리
- 상단 스크롤 시 loadMoreMessages로 이전 메시지 로드
- 커서(timestamp) 기반 페이징 및 hasMore/nextCursor 상태 갱신
- messageId 기반 중복 제거, prepend 시 스크롤 위치 보정
2025-08-13 23:30:41 +09:00
637595e8cd feat(chat-room): 7.1 초기 데이터 로딩 구현 및 ServerChatMessage 매퍼 추가
- enterChatRoom API 연동하여 캐릭터/메시지 초기 로딩
- ServerChatMessage -> ChatMessage 매퍼 추가(toDomain)
- ChatRoomActivity에서 어댑터에 초기 메시지 세팅 및 헤더 갱신
- hasMore/nextCursor 상태 갱신 및 오류 처리
2025-08-13 23:26:01 +09:00
ceae25ea06 feat(chat-room): 메시지 입력/전송/실패 처리(6.1~6.3) 구현
- 왜: 채팅방에서 메시지 입력/전송 및 오류 대응 UX 완성을 위해 6.x 과업을 구현했습니다.
- 무엇:
  - 6.1 입력창 UI
    - EditText placeholder 리소스(@string/chat_input_placeholder) 적용, 최대 200자 제한
    - imeOptions(actionSend|flagNoEnterAction)로 IME 전송 액션 지원
    - 전송 버튼 활성/비활성 상태 관리(TextWatcher), 접근성 라벨(@string/action_send)
    - 입력창 포커스/클릭 시 키보드 표시, 전송 후 키보드 숨김
  - 6.2 전송 플로우
    - onSendClicked()/sendMessage() 도입: 즉시 SENDING 상태로 사용자 메시지 추가
    - 타이핑 인디케이터 표시/숨김 제어(ChatMessageAdapter.show/hideTypingIndicator)
    - 성공 시뮬레이션 후 SENT로 상태 업데이트 및 AI 응답 메시지 추가
    - TODO: 실제 TalkApi POST 연동 지점 주석 추가
  - 6.3 전송 실패 처리
    - FAILED 상태 시 사용자 메시지에 재전송 버튼 노출(item_chat_user_message.xml: iv_retry)
    - 어댑터 콜백을 통한 onRetrySend(localId) 처리 → 재시도 시 SENDING → SENT(성공 시)로 전환
    - strings: action_retry 추가, 접근성 라벨 적용
2025-08-13 23:10:32 +09:00
0cf0d2e790 feat(chat-room-ui): 5.1~5.5 구현 - Activity 구조/헤더/안내/배경 및 스크롤
왜: 채팅방 UI tasks 5를 완료하여 기본 화면 구성을 완성하고 사용자 경험을 개선하기 위함

무엇: \n- 5.1 기본 Activity 구조 구현 (roomId 처리, setupView 골격)\n- 5.2 RecyclerView 설정 및 무한 스크롤/자동 스크롤/상단 prepend 보정 로직\n- 5.3 헤더 영역: 뒤로가기, 프로필(CoIL), 이름, 타입 배지(기존 배경 리소스)\n- 5.4 안내 메시지: SharedPreferences로 접기 상태 저장, 캐릭터 타입별 안내, strings 리소스 사용\n- 5.5 배경 프로필 이미지 로딩 및 딤 처리 적용(레이아웃 구성 활용)

추가: 관련 문서 docs/ (5.1/5.2/5.3/5.4/5.5, notice strings) 작성 및 정리
2025-08-13 21:37:42 +09:00
45b76da1e8 feat(chat-room-ui): ChatMessageAdapter 구현 2025-08-13 21:08:01 +09:00
9bb8dcd881 feat(chat-room-ui): 사용자 메시지, AI 메시지 아이템 레이아웃, 타이핑 인디케이터 아이템 레이아웃 및 애니메이션 추가
item_chat_user_message.xml
- 오른쪽 정렬된 메시지 버블 구현
- 버블 왼쪽에 시간 텍스트(tv_time) 배치
- bg_chat_user_message 배경 및 패딩 적용
- 텍스트 접근성과 가독성 향상을 위한 속성 설정

item_chat_ai_message.xml
- 왼쪽 정렬된 메시지, 프로필 이미지와 이름, 오른쪽 시간 표시 구조 구현
- 그룹화 대응을 위한 조건부 표시(View visibility) 구조 마련
- bg_chat_ai_message 배경과 가독성 개선 속성 적용

item_chat_typing_indicator.xml, typing_dots_animation.xml
- AI 메시지와 동일한 좌측 정렬 구조에 3개 점 애니메이션 영역 구현
- 600ms alpha 애니메이션 반복으로 로딩 상태 시각화
- 추후 ViewHolder에서 점별 startOffset 설정을 통해 순차 반짝임 완성 예정
2025-08-13 20:30:07 +09:00
760cbb8228 feat(chat-room-ui): implement main chat room layout (task 3.1) 2025-08-13 20:14:51 +09:00
4a214523c6 feat(chat): 채팅 문자열 리소스 추가 - task 2.3 완료 (chat_notice_clone, chat_notice_character, chat_input_placeholder) - requirements 6.1/6.2, 4.2 충족 - 파일: app/src/main/res/values/strings.xml 2025-08-13 19:58:11 +09:00
6345b1dbee feat(chat): 타이핑 인디케이터 애니메이션 추가\n\n- task 2.2 완료: typing_dots_animation.xml(alpha, 600ms, reverse, infinite) 생성\n- 사용자 메시지 전송 후 AI 응답 대기 시 점(•••) 순차 반짝임 효과 제공\n- 파일: app/src/main/res/anim/typing_dots_animation.xml\n\n왜: 사용자 메시지 전송 직후 로딩 상태를 시각적으로 표시하기 위함\n무엇: 세 점에 동일 애니메이션을 적용하고 startOffset(0/200/400ms)으로 시퀀싱하여 반짝임 구현\n관련: .kiro/specs/chat-room-ui/tasks.md 2.2, design.md 453~464 2025-08-13 19:56:59 +09:00
228acadf5a feat(chat-ui): 채팅 메시지 배경 drawable 추가 (Task 2.1)
- 사용자/AI/입력/안내 배경 리소스 생성
- 기존 라운드 리소스 재활용 및 불투명도 적용
- 요구사항 2,6 및 디자인 가이드 반영
- docs: Task 2.1 수행 내역 문서 추가 (docs/chat-room-ui-2.1-drawables.md)
2025-08-13 19:41:33 +09:00
6388895e6e feat(chat-room): ChatRepository 도입 및 TalkApi에 입장/메시지 조회 API 추가
- Repository 패턴 구현: 로컬 DB(Room) + 네트워크(TalkApi) 통합
- enterChatRoom, loadMoreMessages, clearAllMessagesOnLogout 제공
- TalkApi에 /enter, /messages 엔드포인트 추가
- Entity↔도메인 매퍼 추가
- Koin 모듈에 ChatRepository 바인딩
2025-08-13 17:30:04 +09:00
725c4335e1 feat(chat-talk-room): Room Database 설정 및 Entity 생성
refactor(chat-talk-room): 패키지 chat.room → chat.talk.room 마이그레이션 및 DI 모듈 분리

왜: 기능 영역 명확화(talk) 및 DI 책임 분리로 유지보수성과 확장성을 높이기 위함
무엇:
- 모델/응답/enum 파일들을 chat.room → chat.talk.room 으로 이동
- Room DB 패키지를 chat.room.db → chat.talk.room.db 로 이동
- AppDatabase 클래스명을 역할에 맞게 ChatMessageDatabase로 변경

문서:
- docs/chat-talk-room-package-migration-and-di-module.md 추가
- docs/chat-room-room-database.md 내용 클래스명/경로 갱신
2025-08-13 17:10:06 +09:00
64deadda0b feat(chat-room): 1.1 데이터 모델 생성 및 채팅 메시지 모델 서버-로컬 분리
왜: 서버 스키마와 클라이언트 전용 필드가 혼재되어 혼란을 야기하던 문제를 해결하고, 유지보수성과 확장성을 높이기 위함.

무엇:
- tasks 1.1 수행 (데이터 모델 클래스 생성)
  - ChatMessage 데이터 클래스 생성 (로컬/UI/도메인용)
  - MessageStatus enum 생성 (SENDING, SENT, FAILED)
  - MessageType enum 생성 (USER_MESSAGE, AI_MESSAGE, NOTICE, TYPING_INDICATOR)
  - CharacterType 기존 enum 재사용 (chat/character/detail/CharacterDetailResponse.kt)
  - ChatRoomEnterResponse, ChatMessagesResponse 데이터 클래스 생성
- 채팅 메시지 모델 서버-로컬 분리 및 응답 모델 정리
  - ServerChatMessage DTO 추가 (서버 응답 전용: messageId, message, profileImageUrl, mine, createdAt)
  - ChatMessageMappers 추가: ServerChatMessage.toLocal(isGrouped: Boolean = false)
  - ChatRoomEnterResponse, ChatMessagesResponse에서 messages 타입을 List<ServerChatMessage>로 정리
- 문서
  - docs/chat-room-data-models.md 갱신 (서버/로컬 분리 사항 반영)
  - docs/chat-room-message-model-separation.md 신설 (분리 배경/가이드)

추가 참고:
- 시간 포맷 유틸은 후속 태스크(8.1)에서 테스트와 함께 구현 예정
2025-08-13 05:23:12 +09:00
558f74d861 feat(chat): 캐릭터 상세에서 채팅방 생성 후 ChatRoomActivity로 네비게이션 추가
- ChatRoomActivity에 EXTRA_ROOM_ID 및 newIntent 추가
- CharacterDetailActivity에서 chatRoomId 수신 시 화면 이동 처리
- 이벤트 소비 유지로 중복 네비게이션 방지
2025-08-13 02:21:43 +09:00
4eedecd1ce feat(chat-character): 채팅 톡 탭
- 데이터가 없으면 "대화 중인 톡이 없습니다" 메시지 표시
2025-08-13 01:23:56 +09:00
08f9d398c4 feat(chat-character): 캐릭터 상세
- 원작의 UI 레벨을 세계관 하위로 이동
2025-08-13 01:17:27 +09:00
f102c84ea6 feat(chat-character): 캐릭터 탭 모든 액션
- 로그인과 본인인증이 되어 있어야 가능하도록 수정
2025-08-13 01:09:34 +09:00
0c3bca0f9e feat(chat-character): 캐릭터 상세 페이지 API 연동 및 UI 상태 처리
- CharacterApi에 캐릭터 상세 조회 엔드포인트 추가
- CharacterDetailRepository 생성 및 Koin DI 등록
- CharacterDetailViewModel에서 실제 API 호출/로딩/에러 상태 관리
- CharacterDetailActivity에서 loadMock 제거 후 load 호출, Koin 주입으로 전환
- 로딩 다이얼로그 및 에러 토스트 처리 로직 추가
2025-08-13 00:52:24 +09:00
ff1e134fe4 feat(character list): 캐릭터 탭
- 배너 리스트 추가
- 배너, 캐릭터 클릭시 캐릭터 상세 페이지로 이동
2025-08-13 00:05:39 +09:00
d8b48fe362 feat(character list): 캐릭터 이미지 배경색 제거 2025-08-12 23:39:22 +09:00
ac2482a645 feat(character detail): 캐릭터 상세 페이지 UI 추가 2025-08-12 22:15:52 +09:00
5090809be8 gitignore 규칙 추가
- .kiro/
2025-08-11 15:44:05 +09:00
80c593bc11 fix: 채팅방 리스트 API URL 수정
- /api/chat/talk/rooms -> /api/chat/room/list
2025-08-11 14:55:31 +09:00
18b61ab74f fix: 채팅 탭 data class
- SerializedName 추가
2025-08-11 11:24:10 +09:00
ea22c7244c feat(ui): 캐릭터 탭
- loadingDialog, Toast 라이브 데이터 옵저버 연결
2025-08-05 02:07:46 +09:00
b1c9c3e124 feat(ui): 톡 탭
- api, viewmodel, repository 연결
- 채팅방 리스트 UI 추가
2025-08-05 02:01:19 +09:00
93fc837b7a feat(ui): 캐릭터 탭
- 섹션별로 데이터가 있으면 보여주고 없으면 UI를 제거하도록 로직 추가
2025-08-04 23:38:51 +09:00
f0eda41c7c feat(ui): viewmodel, repository, api 추가 2025-08-04 22:24:13 +09:00
47717002e8 feat(ui): banner 추가 2025-08-04 22:10:27 +09:00
7b7513561d refactor: item decoration 추가 2025-08-04 22:04:19 +09:00
33bdaa7dbd refactor: 캐릭터 탭 내부에서 사용하는 Adapter 코드를 ViewBinding 코드로 리팩토링 2025-08-04 21:02:05 +09:00
b919691689 feat(character): 캐릭터 탭 UI 및 기본 기능 구현 2025-08-04 20:27:33 +09:00
e90222e8db feat(ui): 채팅 탭 내 TabLayout 캐릭터, 톡 탭 추가 2025-08-01 19:25:14 +09:00
3cf57c1f91 feat(ui): 채팅 탭 추가 2025-08-01 14:47:51 +09:00
f6e7229246 chore: .gitignore 파일에 .idea 관련 파일 추가 2025-08-01 14:34:58 +09:00
f55e74c8dc feat: git 제외 파일 및 폴더
- docs
- junie guidelines
2025-07-31 20:16:25 +09:00
e25276658d feat: 마이페이지
- 내 채널 보기 추가
2025-07-30 14:52:59 +09:00
d088c6f6b3 # 고객센터 UI 개선 및 버전 업데이트 (v1.41.0)
## 변경사항
- 앱 버전을 1.40.0(179)에서 1.41.0(181)으로 업데이트
- 고객센터 화면에 전용 로고 이미지 추가 및 UI 개선
  - 플레이스홀더 이미지를 고객센터 전용 로고로 교체
  - 텍스트 마진 조정 (13.3dp → 16dp)
- 마이페이지 화면 UI 개선
  - 본인인증 버튼 텍스트 간소화 ("본인인증 완료" → "인증완료")
  - 레이아웃 구조 개선 (패딩/마진 조정)
  - RecyclerView 스크롤 경험 개선 (clipToPadding 속성 추가)
2025-07-28 17:33:03 +09:00
9361610647 feat: 마이페이지
- 상단에 최신 공지사항 추가
2025-07-25 22:18:22 +09:00
7ed5e921bd feat: 마이페이지
- 최근 들은 콘텐츠 추가
2025-07-25 21:36:34 +09:00
39be49b481 feat: 마이페이지
- 신규 UI 적용
2025-07-25 16:52:34 +09:00
3b7b5f98bd fix: 메인 라이브 - 최근 종료한 라이브
- 이미지 사이즈 72 -> 84
2025-07-21 20:07:46 +09:00
9be1b86c5d fix: 메인 홈 - 인기 크리에이터
- 팔로우/팔로잉 배경색 변경
2025-07-21 19:52:23 +09:00
cfe9d3ab11 fix: 메인 라이브 - 최근 종료한 라이브
- 비로그인 상태에서 터치시 로그인 페이지로 이동
2025-07-21 18:56:44 +09:00
accb413636 feat: 메인 홈 - 오디션
- 비로그인 상태에서 터치시 로그인 페이지로 이동
2025-07-21 18:51:01 +09:00
bdac7b7899 feat: 메인 홈 - 인기 크리에이터
- 팔로우/언팔로우 기능 추가
2025-07-21 18:48:09 +09:00
58bc42cc0f feat: 메인 라이브 - 최근 종료한 라이브
- 사용 하지 않는 데이터 제거
2025-07-21 18:24:53 +09:00
44d7ce65ae feat: 메인 라이브
- 신규 UI 적용
2025-07-21 18:00:31 +09:00
c55cc68f5c feat: 메인 라이브, 메인 홈
- 섹션 제목 크기 26 -> 24
- 오디션 배너 변경
- 추천 채널 아이템 bg 톤다운
2025-07-19 04:02:29 +09:00
d7cc874684 feat: 메인 라이브
- 최근 종료한 라이브, 라이브 다시 듣기, 라이브 예약 아이템 사이즈 조절
2025-07-19 02:17:47 +09:00
f1164bbd30 feat: 메인 라이브 - 지금 라이브 중
- bg => #263238로 변경
- 가로 => 168 -> 144
- 세로 => 238 -> 204
2025-07-19 01:26:21 +09:00
5f6d26c83e feat: 메인 라이브
- 최근 종료한 라이브 - 라이브 아이콘 제거
- 커뮤니티 - 이미지 사이즈 수정 (53.3 -> 64)
2025-07-18 23:07:19 +09:00
fcd341a1f4 feat: 메인 라이브
- 예약 라이브 - 유료 라이브 금액 나오지 않던 버그 수정
- 팔로잉 채널 - 위치 커뮤니티와 지금 라이브 중 사이로 이동
2025-07-18 22:51:21 +09:00
6e5a4cff45 feat: 메인 라이브
- 변경된 커뮤니티 게시글 아이템 UI 적용
2025-07-18 21:37:16 +09:00
45fd75ab36 feat: 메인 홈
- 오디션 리스트를 보여주지 않고 터치시 오디션 페이지로 이동하도록 수정
2025-07-18 21:06:13 +09:00
2f9bace3de feat: 메인 라이브
- 라이브 다시 듣기 UI 추가
2025-07-18 20:43:30 +09:00
964f697466 feat: 메인 라이브
- 개편된 지금 라이브 중 UI 적용
2025-07-18 19:21:20 +09:00
bb23f9cf93 feat: 메인 라이브
- 최근 종료한 라이브 UI 추가
2025-07-18 18:57:11 +09:00
440104a7d1 feat: 메인 라이브
- 라이브 예약 중 UI 변경
2025-07-17 20:49:44 +09:00
0c7c7946c6 feat: 메인 라이브
- 새로운 UI의 기본 골격 적용
2025-07-16 22:07:07 +09:00
386f9aae32 feat: 메인 홈
- 섹션 간의 간격 수정
- 기존: 밑에 있는 섹션에서 marginTop="48dp"
- 변경: 위에 있는 섹션에서 marginBottom=48dp"
2025-07-16 16:24:07 +09:00
b5d0309f2b feat: 메인 홈
- 돋보기 터치시 검색 페이지 연결
2025-07-16 14:13:18 +09:00
3e525b05a5 feat: 메인 홈
- UI 수정
2025-07-15 21:42:56 +09:00
141e7fe416 feat: 메인 홈
- 다른 페이지로 이동시 로그인 안되어 있으면 로그인 페이지로 이동
2025-07-15 20:41:35 +09:00
db2e3bc8f2 feat: 메인 홈
- 추천 채널 UI 추가
2025-07-15 20:20:54 +09:00
66a6f4bbab feat: 메인 홈
- 큐레이션 UI 추가
2025-07-15 19:01:29 +09:00
a328ea9c3c feat: 메인 홈
- 무료 콘텐츠 UI 추가
2025-07-15 18:44:14 +09:00
76b8b74d41 feat: 메인 홈
- 보온 주간 차트 UI 추가
2025-07-15 18:34:46 +09:00
5c4141dad9 feat: 메인 홈
- 요일별 시리즈 UI 추가
2025-07-15 17:54:53 +09:00
e787872cc5 feat: 메인 홈
- 오디션 배너 UI 추가
2025-07-15 16:27:14 +09:00
af818bda93 feat: 메인 홈
- 오직 보이스온에서만 UI 추가
2025-07-15 16:08:25 +09:00
ccc774da0d feat: 메인 홈 - 최신 콘텐츠
- 데이터가 1개만 있을 때도 2줄 영역을 차지하던 버그 수정
2025-07-15 15:28:50 +09:00
32d61d9808 feat: 메인 홈
- 이벤트 배너 UI 추가
2025-07-15 06:34:41 +09:00
83a30fa088 feat: 메인 홈
- 최신 콘텐츠 UI 추가
2025-07-15 06:27:33 +09:00
f24cd97afa feat: 메인 홈
- 인기 크리에이터 UI 추가
2025-07-15 05:39:04 +09:00
388770889f feat: 메인 홈
- 라이브 UI 추가
2025-07-15 05:04:21 +09:00
e3121fc49b feat: 스플래시 변경 2025-07-14 21:47:59 +09:00
f1958995f6 feat: 하단 탭 아이콘 변경 2025-07-07 20:23:23 +09:00
ba7b681e48 feat: 커뮤니티 전체보기
- gif 재생 되도록 추가
2025-07-03 14:33:45 +09:00
e4012a1301 feat: 커뮤니티 글쓰기/수정
- 이미지 gif 등록 기능 추가
2025-07-03 13:15:01 +09:00
6ff0d8bd61 fix: 사용하지 않는 퍼미션 제거
- GET_ACCOUNTS
2025-06-16 16:09:37 +09:00
898afc78ef fix: 커뮤니티 댓글
- 무료 커뮤니티 글, 내 커뮤니티 글 에서 비밀댓글 체크박스가 보이지 않도록 수정
2025-06-13 21:06:03 +09:00
c527f55721 feat: 팔로워 리스트
- 프로필 이미지를 터치하면 프로필 다이얼로그 표시
2025-06-13 19:36:38 +09:00
89277c5668 feat: 커뮤니티 댓글 리스트
- 비밀댓글 태그 추가
2025-06-13 17:07:16 +09:00
28388497b8 feat: 커뮤니티 댓글
- 유료 커뮤니티 구매시 비밀 댓글 쓰기 기능 추가
2025-06-13 16:52:40 +09:00
09a2a96596 refactor: 콘텐츠 상세
- cleanup code를 실행하여 불필요한 코드 제거
2025-06-12 16:16:52 +09:00
d3f6a02be2 feat: 쿠폰 등록, 인기 단편 전체보기
- 쿠폰 등록 후 캔 내역 페이지가 아닌 바로 이전 페이지로 이동하도록 수정
- 인기 단편 전체보기에 포인트 사용 여부 표시
2025-06-10 20:49:06 +09:00
c8cc0457e4 feat: 쿠폰 등록 안내 문구 수정 2025-06-10 19:57:56 +09:00
4d9e68d60b feat: 시리즈 상세 - 콘텐츠 리스트
- 포인트 사용 가능 여부 표시
2025-06-10 18:24:46 +09:00
74585bfb7f feat: 크리에이터 채널 - 콘텐츠 리스트
- 포인트 사용 가능 여부 표시
2025-06-10 14:54:20 +09:00
ea766afba9 feat: 콘텐츠 메인 - 새로운 콘텐츠, 큐레이션
- 포인트 사용이 가능한 콘텐츠의 썸네일 우측 상단에 포인트 사용 가능 표시
2025-06-10 12:26:04 +09:00
f10d848797 feat: 콘텐츠 메인 - 채널별 인기 콘텐츠
- 포인트 사용이 가능한 콘텐츠의 썸네일 우측 상단에 포인트 사용 가능 표시
2025-06-10 12:03:35 +09:00
3bda97b0a7 feat: 콘텐츠 수정
- 태그 수정 기능 추가
- 포인트 사용여부 수정 기능 추가
2025-06-04 20:03:42 +09:00
19c39f636d feat: 콘텐츠 업로드
- 포인트 사용 가능 여부 추가
2025-06-02 15:22:29 +09:00
8b7894a370 feat: 라이브 후원 메시지 글자 수 조정
- 200자 -> 1000자
2025-05-23 19:26:55 +09:00
d1056bda99 feat: 구매 확인 Dialog
- 포인트 사용이 가능한 경우 포인트를 같이 표시하도록 수정
2025-05-20 18:40:41 +09:00
5dbf9bd987 fix: 앱 실행시 처음 실행하는 유저 정보 조회
- point를 가져와서 SharedPreferences에 저장
2025-05-20 18:03:28 +09:00
23494d0936 feat: 포인트 소멸 안내 메시지 추가 2025-05-20 17:43:15 +09:00
116d4b3ecf feat: 포인트 내역 UI 추가 2025-05-20 00:29:00 +09:00
8b8f5b80b8 fix: 로그아웃시 UserDefaults에서 푸시토큰을 삭제하지 않도록 수정 2025-05-17 21:59:28 +09:00
0b9abf39f1 refactor: 라이브 연속 참여 시간 계산시 initialDelay와 period에 있는 같은 값을 period 변수로 선언 2025-05-17 21:43:45 +09:00
9260d271a7 feat: 라이브 30분 연속 청취시 트래킹 API 호출 기능 추가 2025-05-17 16:57:12 +09:00
588 changed files with 27243 additions and 13855 deletions

9
.gitignore vendored
View File

@@ -44,6 +44,7 @@ captures/
# IntelliJ
*.iml
.idea/deviceManager.xml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
@@ -57,6 +58,9 @@ captures/
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
.idea/AndroidProjectSystem.xml
.idea/runConfigurations.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
@@ -140,6 +144,7 @@ output.json
hs_err_pid*
### Kotlin ###
.kotlin/
# Compiled class file
# Log file
@@ -306,4 +311,8 @@ fabric.properties
app/debug/
app/release/
docs/
.junie/
.kiro/
# End of https://www.toptal.com/developers/gitignore/api/macos,android,androidstudio,visualstudiocode,git,kotlin,java

View File

@@ -1,5 +1,40 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>

View File

@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-10-14T08:13:14.161127Z">
<DropdownSelection timestamp="2025-10-23T14:41:22.468459Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=2cec640c34017ece" />
<DeviceId pluginId="PhysicalDevice" identifier="serial=ce0917195d15ab39017e" />
</handle>
</Target>
</DropdownSelection>

View File

@@ -1,27 +1,28 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.gms.google-services'
id 'com.google.android.gms.oss-licenses-plugin'
id 'kotlin-kapt'
id 'com.google.devtools.ksp'
id 'kotlin-parcelize'
id 'org.jlleitschuh.gradle.ktlint'
id 'io.objectbox'
id 'com.google.firebase.crashlytics'
}
android {
namespace 'kr.co.vividnext.sodalive'
compileSdk 34
compileSdk = 35
viewBinding {
enabled true
}
buildFeatures {
dataBinding true
buildConfig true
}
dependenciesInfo {
@@ -31,12 +32,39 @@ android {
includeInBundle = false
}
packaging {
// JNI(.so) 관련
jniLibs {
// pickFirsts: 충돌 시 첫 파일만 채택
pickFirsts += ["**/libaosl.so"]
}
// 일반 리소스(META-INF 등) 관련
resources {
// pickFirsts: 충돌 시 첫 파일만 채택
pickFirsts += [
"META-INF/LICENSE.txt",
"META-INF/NOTICE*"
]
// 자주 쓰는 제외/병합 예시
excludes += [
"META-INF/DEPENDENCIES",
"META-INF/AL2.0",
"META-INF/LGPL2.1"
]
merges += [
"META-INF/services/**"
]
}
}
defaultConfig {
applicationId "kr.co.vividnext.sodalive"
minSdk 23
targetSdk 34
versionCode 165
versionName "1.36.0"
targetSdk 35
versionCode 204
versionName "1.44.0"
}
buildTypes {
@@ -54,17 +82,18 @@ android {
buildConfigField 'String', 'NOTIFLY_PASSWORD', '"c6c585db0aaa4189be44d0467c7d66b6@A"'
buildConfigField 'String', 'KAKAO_APP_KEY', '"231cf78acfa8252fca38b9eedf87c5cb"'
buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"983594297130-5hrmkh6vpskeq6v34350kmilf74574h2.apps.googleusercontent.com"'
buildConfigField 'String', 'APPSCHEME', '"voiceon"'
manifestPlaceholders = [
URISCHEME : "voiceon",
APPLINK_HOST : "voiceon.onelink.me",
FACEBOOK_APP_ID : "612448298237287",
FACEBOOK_CLIENT_TOKEN: "32af760f4a7b7cb7e3b1e7ffd0b0da70",
KAKAO_APP_KEY: "231cf78acfa8252fca38b9eedf87c5cb"
KAKAO_APP_KEY : "231cf78acfa8252fca38b9eedf87c5cb"
]
}
debug {
minifyEnabled true
minifyEnabled false
debuggable true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
applicationIdSuffix '.debug'
@@ -79,12 +108,13 @@ android {
buildConfigField 'String', 'NOTIFLY_PASSWORD', '"c6c585db0aaa4189be44d0467c7d66b6@A"'
buildConfigField 'String', 'KAKAO_APP_KEY', '"20cf19413d63bfdfd30e8e6dff933d33"'
buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"758414412471-mosodbj2chno7l1j0iihldh6edmk0gk9.apps.googleusercontent.com"'
buildConfigField 'String', 'APPSCHEME', '"voiceon-test"'
manifestPlaceholders = [
URISCHEME : "voiceon-test",
APPLINK_HOST : "voiceon-test.onelink.me",
FACEBOOK_APP_ID : "608674328645232",
FACEBOOK_CLIENT_TOKEN: "3775e6ea83236a685d264b6c5a1bbb4d",
KAKAO_APP_KEY: "20cf19413d63bfdfd30e8e6dff933d33"
KAKAO_APP_KEY : "20cf19413d63bfdfd30e8e6dff933d33"
]
}
}
@@ -92,9 +122,6 @@ android {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
lint {
checkDependencies true
checkReleaseBuilds false
@@ -102,17 +129,17 @@ android {
}
dependencies {
implementation "androidx.media:media:1.7.0"
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation "androidx.media:media:1.7.1"
implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.recyclerview:recyclerview:1.4.0'
implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'androidx.webkit:webkit:1.12.1'
implementation 'androidx.webkit:webkit:1.14.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.7'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4'
// Logger
implementation("com.orhanobut:logger:2.2.0") {
@@ -132,29 +159,29 @@ dependencies {
}
// Gson
implementation "com.google.code.gson:gson:2.10.1"
implementation "com.google.code.gson:gson:2.13.2"
// Network
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "com.squareup.retrofit2:adapter-rxjava3:2.9.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.3"
implementation "com.squareup.retrofit2:retrofit:3.0.0"
implementation "com.squareup.retrofit2:converter-gson:3.0.0"
implementation "com.squareup.retrofit2:adapter-rxjava3:3.0.0"
implementation "com.squareup.okhttp3:logging-interceptor:5.2.1"
// RxJava3
implementation "io.reactivex.rxjava3:rxjava:3.1.6"
implementation "io.reactivex.rxjava3:rxjava:3.1.12"
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// permission
implementation "io.github.ParkSangGwon:tedpermission-normal:3.3.0"
implementation "io.github.ParkSangGwon:tedpermission-normal:3.4.2"
implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.github.yalantis:ucrop:2.2.11'
implementation 'com.github.zhpanvip:bannerviewpager:3.5.7'
implementation 'com.google.android.gms:play-services-oss-licenses:17.1.0'
// Firebase
implementation platform('com.google.firebase:firebase-bom:32.2.2')
implementation platform('com.google.firebase:firebase-bom:33.16.0')
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
implementation 'com.google.firebase:firebase-messaging-ktx'
@@ -168,36 +195,32 @@ dependencies {
implementation "io.github.bootpay:android:4.4.3"
// agora
implementation "io.agora.rtc:voice-sdk:4.2.6"
implementation 'io.agora.rtm:rtm-sdk:1.5.3'
// sound visualizer
implementation "com.gauravk.audiovisualizer:audiovisualizer:0.9.2"
implementation "io.agora.rtc:voice-sdk:4.5.2"
implementation 'io.agora:agora-rtm:2.2.6'
// Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
implementation "com.michalsvec:single-row-calednar:1.0.0"
implementation 'com.github.bumptech.glide:glide:5.0.5'
annotationProcessor 'com.github.bumptech.glide:compiler:5.0.5'
// google in-app-purchase
implementation "com.android.billingclient:billing-ktx:6.2.0"
implementation "com.android.billingclient:billing-ktx:8.0.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"
ksp "androidx.room:room-compiler:2.8.3"
implementation "androidx.room:room-ktx:2.8.3"
implementation "androidx.room:room-runtime:2.8.3"
implementation "androidx.room:room-rxjava3:2.8.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
implementation "androidx.media3:media3-session:1.4.1"
implementation "androidx.media3:media3-exoplayer:1.4.1"
implementation "androidx.media3:media3-session:1.8.0"
implementation "androidx.media3:media3-exoplayer:1.8.0"
// Facebook
implementation "com.facebook.android:facebook-core:18.0.0"
// Appsflyer
implementation 'com.appsflyer:af-android-sdk:6.16.1'
implementation 'com.appsflyer:af-android-sdk:6.17.4'
// 노티플라이
implementation 'com.github.team-michael:notifly-android-sdk:1.12.0'
@@ -206,4 +229,35 @@ dependencies {
implementation "com.kakao.sdk:v2-common:2.21.0"
implementation "com.kakao.sdk:v2-auth:2.21.0"
implementation "com.kakao.sdk:v2-user:2.21.0"
implementation 'io.github.glailton.expandabletextview:expandabletextview:1.0.4'
implementation 'com.github.orbitalsonic:Sonic-Water-Wave-Animation:2.0.1'
// ----- Test dependencies -----
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.20.0'
testImplementation 'org.mockito:mockito-inline:5.2.0'
testImplementation 'org.mockito.kotlin:mockito-kotlin:6.1.0'
testImplementation 'io.mockk:mockk:1.14.6'
}
// KSP args for Room schema export
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
arg("room.expandProjection", "true")
}
// Kotlin compiler and toolchain configuration (migrated from deprecated kotlinOptions.jvmTarget)
kotlin {
// Ensures Kotlin compiles with Java 17 toolchain
jvmToolchain(17)
// New DSL replacing kotlinOptions.jvmTarget
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}

View File

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

View File

@@ -237,3 +237,9 @@
-dontwarn org.bouncycastle.jsse.**
-dontwarn org.conscrypt.*
-dontwarn org.openjsse.**
-keep interface kr.co.vividnext.sodalive.tracking.UserEventApi
-dontwarn com.yalantis.ucrop**
-keep class com.yalantis.ucrop** { *; }
-keep interface com.yalantis.ucrop** { *; }

1
app/schemas/.gitkeep Normal file
View File

@@ -0,0 +1 @@
# Keep schemas directory under version control

View File

@@ -0,0 +1,76 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "b9a331035b36b70f8ca7a14962b13fdf",
"entities": [
{
"tableName": "playback_tracking",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contentId` INTEGER NOT NULL, `totalDuration` INTEGER NOT NULL, `startPosition` INTEGER NOT NULL, `isFree` INTEGER NOT NULL, `isPreview` INTEGER NOT NULL, `endPosition` INTEGER, `playDateTime` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contentId",
"columnName": "contentId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "totalDuration",
"columnName": "totalDuration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "startPosition",
"columnName": "startPosition",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isFree",
"columnName": "isFree",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPreview",
"columnName": "isPreview",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "endPosition",
"columnName": "endPosition",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "playDateTime",
"columnName": "playDateTime",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b9a331035b36b70f8ca7a14962b13fdf')"
]
}
}

View File

@@ -0,0 +1,82 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "7429c2998f64cb70e5e8b1d2525a4708",
"entities": [
{
"tableName": "alarms",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `time` INTEGER NOT NULL, `days` TEXT NOT NULL, `contentId` INTEGER NOT NULL, `contentTitle` TEXT NOT NULL, `contentCreatorNickname` TEXT NOT NULL, `volume` INTEGER NOT NULL, `isEnabled` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "days",
"columnName": "days",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "contentId",
"columnName": "contentId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contentTitle",
"columnName": "contentTitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "contentCreatorNickname",
"columnName": "contentCreatorNickname",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "volume",
"columnName": "volume",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isEnabled",
"columnName": "isEnabled",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7429c2998f64cb70e5e8b1d2525a4708')"
]
}
}

View File

@@ -0,0 +1,58 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "e46a8b457c3ea6ceefd0db76bb763056",
"entities": [
{
"tableName": "recent_contents",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contentId` INTEGER NOT NULL, `coverImageUrl` TEXT NOT NULL, `title` TEXT NOT NULL, `creatorNickname` TEXT NOT NULL, `listenedAt` INTEGER NOT NULL, PRIMARY KEY(`contentId`))",
"fields": [
{
"fieldPath": "contentId",
"columnName": "contentId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "coverImageUrl",
"columnName": "coverImageUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "creatorNickname",
"columnName": "creatorNickname",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "listenedAt",
"columnName": "listenedAt",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"contentId"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e46a8b457c3ea6ceefd0db76bb763056')"
]
}
}

View File

@@ -38,7 +38,6 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
@@ -86,6 +85,17 @@
<data android:scheme="${URISCHEME}" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- PayVerse 리다이렉트에 등록할 커스텀 스킴/호스트 -->
<data
android:host="payverse"
android:path="/result"
android:scheme="${URISCHEME}" />
</intent-filter>
</activity>
<activity
android:name=".splash.SplashActivity"
@@ -98,16 +108,20 @@
</activity>
<activity android:name=".main.MainActivity" />
<activity android:name=".user.login.LoginActivity" />
<activity android:name=".audio_content.all.AudioContentAllActivity" />
<activity
android:name=".user.signup.SignUpActivity"
android:windowSoftInputMode="stateVisible" />
<activity android:name=".settings.terms.TermsActivity" />
<activity android:name=".user.find_password.FindPasswordActivity" />
<activity android:name=".mypage.can.status.CanStatusActivity" />
<activity android:name=".mypage.point.PointStatusActivity" />
<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.CanPaymentActivity"
android:launchMode="singleTop" />
<activity android:name=".mypage.can.payment.CanPaymentTempActivity" />
<activity android:name=".mypage.can.coupon.CanCouponActivity" />
<activity android:name=".live.room.create.LiveRoomCreateActivity" />
@@ -150,7 +164,6 @@
<activity android:name=".mypage.profile.ProfileUpdateActivity" />
<activity android:name=".mypage.profile.nickname.NicknameUpdateActivity" />
<activity android:name=".mypage.profile.password.ModifyPasswordActivity" />
<activity android:name=".audio_content.curation.AudioContentCurationActivity" />
<activity android:name=".audio_content.all.AudioContentNewAllActivity" />
<activity android:name=".audio_content.all.AudioContentRankingAllActivity" />
<activity android:name=".audio_content.all.by_theme.AudioContentAllByThemeActivity" />
@@ -166,15 +179,8 @@
<activity android:name=".audition.detail.AuditionDetailActivity" />
<activity android:name=".audition.role.AuditionRoleDetailActivity" />
<activity android:name=".audio_content.main.v2.AudioContentMainActivity" />
<activity android:name=".audio_content.main.v2.alarm.all.AlarmContentAllActivity" />
<activity android:name=".audio_content.main.v2.asmr.AsmrNewContentAllActivity" />
<activity android:name=".audio_content.main.v2.replay.ReplayNewContentAllActivity" />
<activity android:name=".audio_content.main.v2.free.introduce_creator.IntroduceCreatorActivity" />
<activity android:name=".audio_content.main.v2.series.origianl_audio_drama.OriginalAudioDramaContentAllActivity" />
<activity android:name=".audio_content.main.v2.series.completed.CompletedSeriesActivity" />
<activity android:name=".search.SearchActivity" />
<activity android:name=".audition.AuditionActivity" />
<activity android:name=".mypage.alarm.AlarmListActivity" />
<activity android:name=".mypage.alarm.AddAlarmActivity" />
@@ -190,6 +196,9 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".chat.character.newcharacters.NewCharactersAllActivity" />
<activity android:name=".chat.original.detail.OriginalWorkDetailActivity" />
<activity android:name=".audio_content.series.main.SeriesMainActivity" />
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
@@ -203,11 +212,13 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Redirect URI: "kakao${NATIVE_APP_KEY}://oauth" -->
<data android:host="oauth"
<data
android:host="oauth"
android:scheme="kakao${KAKAO_APP_KEY}" />
</intent-filter>
</activity>
@@ -279,5 +290,28 @@
android:name="com.facebook.FacebookActivity"
android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" />
<!-- [END facebook] -->
<!-- Character Detail -->
<activity android:name=".chat.character.detail.CharacterDetailActivity" />
<activity android:name=".chat.talk.room.ChatRoomActivity" />
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<!-- ★ 이 meta-data가 꼭 필요 -->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -0,0 +1,25 @@
<!-- app/src/main/assets/payverse_starter_debug.html -->
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<!-- PayVerse SDK -->
<script src="https://ui.payverseglobal.com/js/payments.js"></script>
</head>
<body>
<script>
// 안드로이드에서 JSON 문자열을 넘기면 이 함수를 호출
function startPay(payloadJson) {
try {
const p = JSON.parse(payloadJson);
// 즉시 실행: 페이지가 열리자마자 결제창 시작
window.payVerse.requestUI(p);
} catch (e) {
console.error('startPay error', e);
alert('결제 초기화에 실패했습니다.');
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,25 @@
<!-- app/src/main/assets/payverse_starter_debug.html -->
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<!-- PayVerse SDK -->
<script src="https://ui-snd.payverseglobal.com/js/payments.js"></script>
</head>
<body>
<script>
// 안드로이드에서 JSON 문자열을 넘기면 이 함수를 호출
function startPay(payloadJson) {
try {
const p = JSON.parse(payloadJson);
// 즉시 실행: 페이지가 열리자마자 결제창 시작
window.payVerse.requestUI(p);
} catch (e) {
console.error('startPay error', e);
alert('결제 초기화에 실패했습니다.');
}
}
</script>
</body>
</html>

View File

@@ -6,28 +6,29 @@ import io.agora.rtc2.Constants
import io.agora.rtc2.IRtcEngineEventHandler
import io.agora.rtc2.RtcEngine
import io.agora.rtm.ErrorInfo
import io.agora.rtm.PublishOptions
import io.agora.rtm.ResultCallback
import io.agora.rtm.RtmChannel
import io.agora.rtm.RtmChannelListener
import io.agora.rtm.RtmClient
import io.agora.rtm.RtmClientListener
import io.agora.rtm.SendMessageOptions
import io.agora.rtm.RtmConfig
import io.agora.rtm.RtmConstants
import io.agora.rtm.RtmEventListener
import io.agora.rtm.SubscribeOptions
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.live.room.LiveRoomRequestType
import kotlin.concurrent.thread
class Agora(
private val uid: Long,
private val context: Context,
private val rtcEventHandler: IRtcEngineEventHandler,
private val rtmClientListener: RtmClientListener
private val rtmEventListener: RtmEventListener
) {
// RTM client instance
private var rtmClient: RtmClient? = null
// 상태 플래그: RTM 로그인 완료 여부
private var rtmLoggedIn: Boolean = false
// RTM channel instance
private var rtmChannel: RtmChannel? = null
private var rtcEngine: RtcEngine? = null
// 상태 플래그: RTM 로그인 진행 중 여부
private var rtmLoginInProgress: Boolean = false
init {
initAgoraEngine()
@@ -35,65 +36,51 @@ class Agora(
private fun initAgoraEngine() {
try {
rtcEngine = RtcEngine.create(
context,
BuildConfig.AGORA_APP_ID,
rtcEventHandler
)
rtcEngine!!.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
rtcEngine!!.setAudioProfile(
Constants.AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO,
Constants.AUDIO_SCENARIO_GAME_STREAMING
)
rtcEngine!!.enableAudio()
rtcEngine!!.enableAudioVolumeIndication(500, 3, true)
rtmClient = RtmClient.createInstance(
context,
BuildConfig.AGORA_APP_ID,
rtmClientListener
)
initRtcEngine()
initRtmClient()
} catch (e: Exception) {
e.printStackTrace()
}
}
fun deInitAgoraEngine() {
if (rtcEngine != null) {
rtcEngine!!.leaveChannel()
thread {
RtcEngine.destroy()
rtcEngine = null
}
}
rtmChannel?.leave(null)
rtmChannel?.release()
rtmClient?.logout(null)
fun deInitAgoraEngine(rtmEventListener: RtmEventListener) {
deInitRtcEngine()
deInitRtmClient(rtmEventListener)
}
fun inputChat(message: String) {
val rtmMessage = rtmClient!!.createMessage()
rtmMessage.text = message
// region RtcEngine
private var rtcEngine: RtcEngine? = null
rtmChannel!!.sendMessage(
rtmMessage,
object : ResultCallback<Void?> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
}
override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorDescription}")
}
}
@Throws(Exception::class)
private fun initRtcEngine() {
Logger.e("initRtcEngine")
rtcEngine = RtcEngine.create(
context,
BuildConfig.AGORA_APP_ID,
rtcEventHandler
)
Logger.e("initRtcEngine - rtcEngine: ${rtcEngine != null}")
rtcEngine!!.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
rtcEngine!!.setAudioProfile(
Constants.AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO,
Constants.AUDIO_SCENARIO_GAME_STREAMING
)
rtcEngine!!.setParameters("{\"che.audio.aiaec.working_mode\":0}")
rtcEngine!!.enableAudio()
rtcEngine!!.enableAudioVolumeIndication(500, 3, true)
}
fun joinRtcChannel(uid: Int, rtcToken: String, channelName: String) {
val state = rtcEngine?.connectionState
val isDisconnected = state == null || state == Constants.CONNECTION_STATE_DISCONNECTED
if (!isDisconnected) {
Logger.e("joinRtcChannel - skip (state=$state)")
return
}
Logger.e("joinRtcChannel - proceed (state=$state) uid=$uid channel=$channelName")
rtcEngine!!.joinChannel(
rtcToken,
channelName,
@@ -102,62 +89,6 @@ class Agora(
)
}
fun createRtmChannelAndLogin(
uid: String,
rtmToken: String,
channelName: String,
rtmChannelListener: RtmChannelListener,
rtmChannelJoinSuccess: () -> Unit,
rtmChannelJoinFail: () -> Unit
) {
rtmChannel = rtmClient!!.createChannel(channelName, rtmChannelListener)
rtmClient!!.login(
rtmToken,
uid,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
rtmChannel!!.join(object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("rtmChannel join - onSuccess")
rtmChannelJoinSuccess()
}
override fun onFailure(p0: ErrorInfo?) {
rtmChannelJoinFail()
}
})
}
override fun onFailure(p0: ErrorInfo?) {
}
}
)
}
fun sendRawMessageToGroup(
rawMessage: ByteArray,
onSuccess: (() -> Unit)? = null,
onFailure: (() -> Unit)? = null
) {
val message = rtmClient!!.createMessage()
message.rawMessage = rawMessage
rtmChannel!!.sendMessage(
message,
object : ResultCallback<Void?> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
onSuccess?.invoke()
}
override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorDescription}")
onFailure?.invoke()
}
}
)
}
fun setClientRole(role: Int) {
rtcEngine!!.setClientRole(role)
}
@@ -170,37 +101,304 @@ class Agora(
rtcEngine?.muteAllRemoteAudioStreams(mute)
}
fun getConnectionState(): Int {
return rtcEngine!!.connectionState
}
fun isRtmLoggedIn(): Boolean {
return rtmLoggedIn
}
fun deInitRtcEngine() {
if (rtcEngine != null) {
rtcEngine!!.leaveChannel()
thread {
RtcEngine.destroy()
rtcEngine = null
}
}
}
// endregion
// region RtmClient
private var rtmClient: RtmClient? = null
private var roomChannelName: String? = null
@Throws(Exception::class)
private fun initRtmClient() {
val rtmConfig = RtmConfig.Builder(BuildConfig.AGORA_APP_ID, uid.toString())
.eventListener(rtmEventListener)
.build()
rtmClient = RtmClient.create(rtmConfig)
}
fun rtmLogin(
rtmToken: String,
channelName: String,
rtmChannelJoinSuccess: () -> Unit,
rtmChannelJoinFail: () -> Unit
) {
// 이미 RTM 로그인 및 구독이 완료된 경우 재호출 방지
if (rtmLoggedIn && roomChannelName == channelName) {
Logger.e("rtmLogin - already logged in and subscribed. skip")
return
}
// 로그인 시도 중이면 재호출 방지
if (rtmLoginInProgress) {
Logger.e("rtmLogin - already in progress. skip")
return
}
roomChannelName = channelName
fun attemptLogin(attempt: Int) {
rtmClient!!.login(
rtmToken,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("rtmClient login - success (attempt=$attempt)")
// 로그인 성공 후 두 채널 구독 시도
subscribeChannel(rtmChannelJoinSuccess, rtmChannelJoinFail)
}
override fun onFailure(p0: ErrorInfo?) {
Logger.e("rtmClient login - fail (attempt=$attempt), ${p0?.errorReason}")
if (attempt < 4) {
attemptLogin(attempt + 1)
} else {
rtmLoginInProgress = false
rtmChannelJoinFail()
}
}
}
)
}
rtmLoginInProgress = true
attemptLogin(1)
}
private fun subscribeChannel(
rtmChannelJoinSuccess: () -> Unit,
rtmChannelJoinFail: () -> Unit
) {
val targetRoom = roomChannelName
if (targetRoom == null) {
Logger.e("subscribeChannel - roomChannelName is null")
rtmChannelJoinFail()
return
}
var completed = false
var roomSubscribed = false
var inboxSubscribed = false
fun completeSuccessIfReady() {
if (!completed && roomSubscribed && inboxSubscribed) {
completed = true
rtmLoggedIn = true
rtmLoginInProgress = false
Logger.e("RTM subscribe - both channels subscribed")
rtmChannelJoinSuccess()
}
}
fun failOnce(reason: String?) {
if (!completed) {
completed = true
Logger.e("RTM subscribe failed: $reason")
rtmChannelJoinFail()
}
}
fun subscribeRoom(attempt: Int) {
val channelOptions = SubscribeOptions()
channelOptions.withMessage = true
channelOptions.withPresence = true
Logger.e("RTM subscribe(room: $targetRoom) attempt=$attempt")
rtmClient!!.subscribe(
targetRoom,
channelOptions,
object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM subscribe(room) success at attempt=$attempt")
roomSubscribed = true
completeSuccessIfReady()
}
override fun onFailure(errorInfo: ErrorInfo?) {
Logger.e("RTM subscribe(room) failure at attempt=$attempt reason=${errorInfo?.errorReason}")
if (attempt < 4) {
subscribeRoom(attempt + 1)
} else {
failOnce("room subscribe failed after 3 retries (4 attempts)")
}
}
}
)
}
fun subscribeInbox(attempt: Int) {
val inboxChannel = "inbox_$uid"
val inboxChannelOptions = SubscribeOptions()
inboxChannelOptions.withMessage = true
Logger.e("RTM subscribe(inbox: $inboxChannel) attempt=$attempt")
rtmClient!!.subscribe(
inboxChannel,
inboxChannelOptions,
object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM subscribe(inbox) success at attempt=$attempt")
inboxSubscribed = true
completeSuccessIfReady()
}
override fun onFailure(errorInfo: ErrorInfo?) {
Logger.e("RTM subscribe(inbox) failure at attempt=$attempt reason=${errorInfo?.errorReason}")
if (attempt < 4) {
subscribeInbox(attempt + 1)
} else {
failOnce("inbox subscribe failed after 3 retries (4 attempts)")
}
}
}
)
}
// 두 채널 구독을 병렬로 시도
subscribeRoom(1)
subscribeInbox(1)
}
fun inputChat(message: String, onFailure: () -> Unit) {
if (roomChannelName != null) {
val options = PublishOptions()
options.setChannelType(RtmConstants.RtmChannelType.MESSAGE)
rtmClient!!.publish(
roomChannelName!!,
message,
options,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
}
override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorReason}")
}
}
)
} else {
Logger.e("inputChat - roomChannelName is null")
onFailure()
}
}
fun sendRawMessageToGroup(
rawMessage: ByteArray,
onSuccess: (() -> Unit)? = null,
onFailure: (() -> Unit)? = null
) {
if (roomChannelName != null) {
val options = PublishOptions()
options.customType = "ByteArray"
rtmClient!!.publish(
roomChannelName!!,
rawMessage,
options,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
onSuccess?.invoke()
}
override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorReason}")
onFailure?.invoke()
}
}
)
} else {
Logger.e("inputChat - roomChannelName is null")
onFailure?.invoke()
}
}
fun sendRawMessageToPeer(
receiverUid: String,
requestType: LiveRoomRequestType? = null,
rawMessage: ByteArray? = null,
onSuccess: () -> Unit
) {
val option = SendMessageOptions()
if (roomChannelName != null) {
val message = rawMessage ?: requestType.toString().toByteArray()
val options = PublishOptions()
options.customType = "ByteArray"
rtmClient!!.publish(
"inbox_$receiverUid",
message,
options,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
onSuccess()
}
val message = rtmClient!!.createMessage()
message.rawMessage = rawMessage ?: requestType.toString().toByteArray()
override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorReason}")
}
}
)
} else {
Logger.e("inputChat - roomChannelName is null")
}
}
rtmClient!!.sendMessageToPeer(
receiverUid,
message,
option,
object : ResultCallback<Void?> {
override fun onSuccess(aVoid: Void?) {
onSuccess()
fun deInitRtmClient(rtmEventListener: RtmEventListener) {
rtmClient?.removeEventListener(rtmEventListener)
rtmClient?.unsubscribe(roomChannelName, object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM unsubscribe - $roomChannelName")
roomChannelName = null
}
override fun onFailure(errorInfo: ErrorInfo) {
Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}")
Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}")
}
})
rtmClient?.unsubscribe(
"inbox_${SharedPreferenceManager.userId}",
object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM unsubscribe - inbox_${SharedPreferenceManager.userId}")
}
override fun onFailure(errorInfo: ErrorInfo) {
Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}")
Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}")
}
})
rtmClient?.logout(object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM logout")
rtmClient = null
}
)
}
fun rtmChannelIsNull(): Boolean {
return rtmChannel == null
}
override fun onFailure(errorInfo: ErrorInfo) {
Logger.e("RTM logout fail - ${errorInfo.errorCode}")
Logger.e("RTM logout fail - ${errorInfo.errorReason}")
}
})
// 상태 리셋
rtmLoggedIn = false
rtmLoginInProgress = false
fun getConnectionState(): Int {
return rtcEngine!!.connectionState
}
// endregion
}

View File

@@ -28,6 +28,12 @@ class AudioContentAdapter(
View.GONE
}
binding.tvPoint.visibility = if (item.isPointAvailable) {
View.VISIBLE
} else {
View.GONE
}
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)

View File

@@ -1,12 +1,12 @@
package kr.co.vividnext.sodalive.audio_content
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audio_content.all.GetNewContentAllResponse
import kr.co.vividnext.sodalive.audio_content.all.by_theme.GetContentByThemeResponse
import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListResponse
import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest
import kr.co.vividnext.sodalive.audio_content.comment.RegisterAudioContentCommentRequest
import kr.co.vividnext.sodalive.audio_content.curation.GetCurationContentResponse
import kr.co.vividnext.sodalive.audio_content.detail.GetAudioContentDetailResponse
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeResponse
@@ -15,21 +15,13 @@ 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.GetAudioContentRanking
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.main.v2.alarm.GetContentMainTabAlarmResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.asmr.GetContentMainTabAsmrResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.content.GetContentMainTabContentResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.free.GetContentMainTabLiveFreeResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.home.GetContentMainTabHomeResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.replay.GetContentMainTabLiveReplayResponse
import kr.co.vividnext.sodalive.audio_content.main.v2.series.GetContentMainTabSeriesResponse
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListResponse
import kr.co.vividnext.sodalive.audio_content.order.OrderRequest
import kr.co.vividnext.sodalive.audio_content.player.GenerateUrlResponse
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListResponse
import kr.co.vividnext.sodalive.home.AudioContentMainItem
import kr.co.vividnext.sodalive.settings.ContentType
import okhttp3.MultipartBody
import okhttp3.RequestBody
@@ -45,6 +37,19 @@ import retrofit2.http.Path
import retrofit2.http.Query
interface AudioContentApi {
@GET("/audio-content/all")
fun getAllAudioContents(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("isFree") isFree: Boolean?,
@Query("isPointAvailableOnly") isPointAvailableOnly: Boolean?,
@Query("sort-type") sortType: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
@Query("theme") theme: String? = null,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<AudioContentMainItem>>>
@GET("/audio-content")
fun getAudioContentList(
@Query("creator-id") id: Long,
@@ -56,11 +61,27 @@ interface AudioContentApi {
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentListResponse>>
@GET("/audio-content/replay-live")
fun getAudioContentReplayLiveList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Flowable<ApiResponse<List<GetAudioContentMainItem>>>
@GET("/audio-content/theme")
fun getAudioContentThemeList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentThemeResponse>>>
@GET("/audio-content/theme/active")
fun getAudioContentActiveThemeList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("isFree") isFree: Boolean?,
@Query("isPointAvailableOnly") isPointAvailableOnly: Boolean?,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<String>>>
@GET("/audio-content/theme/{id}/content")
fun getAudioContentByTheme(
@Path("id") id: Long,
@@ -182,17 +203,6 @@ interface AudioContentApi {
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/curation/{id}")
fun getAudioContentListByCurationId(
@Path("id") id: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort-type") sort: AudioContentViewModel.Sort,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetCurationContentResponse>>
@GET("/audio-content/main/theme")
fun getNewContentThemeList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@@ -249,187 +259,4 @@ interface AudioContentApi {
@Path("id") contentId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GenerateUrlResponse>>
@GET("/v2/audio-content/main/home")
fun getContentMainHome(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabHomeResponse>>
@GET("/v2/audio-content/main/home/popular-content-by-creator")
fun getPopularContentByCreator(
@Query("creatorId") creatorId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/home/content/ranking")
fun getContentMainHomeContentRanking(
@Query("sort-type") sortType: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/series")
fun getContentMainSeries(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabSeriesResponse>>
@GET("/v2/audio-content/main/series/original")
fun getOriginalAudioDramaList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetSeriesListResponse>>
@GET("/v2/audio-content/main/series/recommend-by-genre")
fun getRecommendSeriesListByGenre(
@Query("genreId") genreId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
@GET("/v2/audio-content/main/series/recommend-series-by-creator")
fun getRecommendSeriesByCreator(
@Query("creatorId") creatorId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
@GET("/v2/audio-content/main/series/completed-rank")
fun getCompletedSeries(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetSeriesListResponse>>
@GET("/v2/audio-content/main/content")
fun getContentMainContent(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabContentResponse>>
@GET("/v2/audio-content/main/content/new-content-by-theme")
fun getContentMainNewContentOfTheme(
@Query("theme") theme: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@GET("/v2/audio-content/main/content/ranking")
fun getDailyContentRanking(
@Query("sort-type") sortType: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/content/popular-content-by-creator")
fun getContentMainContentPopularContentByCreator(
@Query("creatorId") creatorId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/alarm")
fun getContentMainAlarm(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabAlarmResponse>>
@GET("/v2/audio-content/main/alarm/all")
fun getContentMainAlarmAll(
@Query("theme") theme: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetNewContentAllResponse>>
@GET("/v2/audio-content/main/asmr")
fun getContentMainAsmr(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabAsmrResponse>>
@GET("/v2/audio-content/main/asmr/popular-content-by-creator")
fun getPopularAsmrContentByCreator(
@Query("creatorId") creatorId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/replay")
fun getContentMainReplay(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabLiveReplayResponse>>
@GET("/v2/audio-content/main/replay/popular-content-by-creator")
fun getPopularReplayContentByCreator(
@Query("creatorId") creatorId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/free")
fun getContentMainFree(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentMainTabLiveFreeResponse>>
@GET("/v2/audio-content/main/free/introduce-creator")
fun getIntroduceCreatorList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@GET("/v2/audio-content/main/free/new-content-by-theme")
fun getNewFreeContentOfTheme(
@Query("theme") theme: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@GET("/v2/audio-content/main/free/popular-content-by-creator")
fun getPopularFreeContentByCreator(
@Query("creatorId") creatorId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
@GET("/v2/audio-content/main/content/recommend-content-by-tag")
fun getRecommendedContentByTag(
@Query("tag") tag: String,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
}

View File

@@ -6,7 +6,6 @@ import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationReque
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.explorer.ExplorerApi
import kr.co.vividnext.sodalive.settings.ContentType
import okhttp3.MultipartBody
import okhttp3.RequestBody
@@ -14,25 +13,8 @@ import java.util.TimeZone
class AudioContentRepository(
private val api: AudioContentApi,
private val categoryApi: CategoryApi,
private val explorerApi: ExplorerApi
private val categoryApi: CategoryApi
) {
fun getAudioContentListByCurationId(
curationId: Long,
page: Int,
size: Int,
sort: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
token: String
) = api.getAudioContentListByCurationId(
id = curationId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
sort = sort,
authHeader = token
)
fun getAudioContentList(
id: Long,
categoryId: Long,
@@ -50,6 +32,12 @@ class AudioContentRepository(
authHeader = token
)
fun getAudioContentReplayLiveList(token: String) = api.getAudioContentReplayLiveList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getAudioContentThemeList(token: String) = api.getAudioContentThemeList(token)
fun uploadAudioContent(
@@ -121,13 +109,6 @@ class AudioContentRepository(
token: String
) = api.likeContent(request, authHeader = token)
fun getNewContentOfTheme(theme: String, token: String) = api.getNewContentOfTheme(
theme = theme,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getNewContentAllOfTheme(
isFree: Boolean,
theme: String,
@@ -138,7 +119,7 @@ class AudioContentRepository(
isFree = isFree,
theme = theme,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
authHeader = token
@@ -146,7 +127,7 @@ class AudioContentRepository(
fun getNewContentThemeList(token: String) = api.getNewContentThemeList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
authHeader = token
)
@@ -178,16 +159,6 @@ class AudioContentRepository(
authHeader = token
)
fun getCurationList(page: Int, size: Int, token: String) = api.getCurationList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
authHeader = token
)
fun getMainBannerList(token: String) = api.getMainBannerList(authHeader = token)
fun getMainOrderList(token: String) = api.getMainOrderList(authHeader = token)
fun pinContent(
audioContentId: Long,
token: String
@@ -212,12 +183,42 @@ class AudioContentRepository(
) = api.getAudioContentByTheme(
id = themeId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
sort = sort,
authHeader = token
)
fun getCreatorRank(token: String) = explorerApi.getCreatorRank(authHeader = token)
fun getAllAudioContents(
page: Int,
size: Int,
isFree: Boolean? = null,
isPointAvailableOnly: Boolean? = null,
sortType: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
theme: String? = null,
token: String
) = api.getAllAudioContents(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
isFree = isFree,
isPointAvailableOnly = isPointAvailableOnly,
sortType = sortType,
theme = theme,
authHeader = token
)
fun getAudioContentActiveThemeList(
isFree: Boolean? = null,
isPointAvailableOnly: Boolean? = null,
token: String
) = api.getAudioContentActiveThemeList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
isFree = isFree,
isPointAvailableOnly = isPointAvailableOnly,
authHeader = token
)
}

View File

@@ -41,7 +41,10 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
PRICE_HIGH,
@SerializedName("PRICE_LOW")
PRICE_LOW
PRICE_LOW,
@SerializedName("POPULARITY")
POPULARITY
}
var isLast = false

View File

@@ -1,16 +1,16 @@
package kr.co.vividnext.sodalive.audio_content
import androidx.annotation.Keep
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Entity
@Entity(tableName = "playback_tracking")
@Keep
data class PlaybackTracking(
@Id
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
var contentId: Long,
var totalDuration: Int,

View File

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

View File

@@ -0,0 +1,226 @@
package kr.co.vividnext.sodalive.audio_content.all
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.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.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.ActivityAudioContentAllBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.home.HomeContentAdapter
import kr.co.vividnext.sodalive.home.HomeContentThemeAdapter
import org.koin.android.ext.android.inject
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class AudioContentAllActivity : BaseActivity<ActivityAudioContentAllBinding>(
ActivityAudioContentAllBinding::inflate
) {
private val viewModel: AudioContentAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: HomeContentAdapter
private lateinit var themeAdapter: HomeContentThemeAdapter
private var isFree: Boolean = false
private var isPointOnly: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
isFree = intent.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, false)
isPointOnly = intent.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_POINT_ONLY, false)
super.onCreate(savedInstanceState)
bindData()
viewModel.reset()
viewModel.getThemeList(
isFree = if (isFree) true else null,
isPointAvailableOnly = if (isPointOnly) true else null
)
viewModel.loadAll(
isFree = if (isFree) true else null,
isPointAvailableOnly = if (isPointOnly) true else null
)
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = when {
isPointOnly -> "포인트 대여 전체"
isFree -> "무료 콘텐츠 전체"
else -> "콘텐츠 전체보기"
}
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.tvSortNewest.setOnClickListener {
viewModel.selectSort(AudioContentViewModel.Sort.NEWEST)
}
binding.tvSortPopularity.setOnClickListener {
viewModel.selectSort(AudioContentViewModel.Sort.POPULARITY)
}
setupTheme()
setupRecycler()
}
private fun setupTheme() {
themeAdapter = HomeContentThemeAdapter {
adapter.addItems(emptyList())
viewModel.selectTheme(it, isFree = isFree, isPointOnly = isPointOnly)
}
binding.rvTheme.layoutManager = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvTheme.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()
}
themeAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvTheme.adapter = themeAdapter
}
private fun setupRecycler() {
// 아이템 정사각형 크기 계산: (screenWidth - (16*2) - 16) / 2
// 아이템 정사각형 크기 계산: (screenWidth - (paddingHorizontal*2) - itemSpacing) / 2
val itemSize = ((screenWidth - 16f.dpToPx() * 2 - 16f.dpToPx()) / 2f).toInt()
adapter = HomeContentAdapter(
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
itemSquareSizePx = itemSize
)
val spanCount = 2
val spacingPx = 16f.dpToPx().toInt()
binding.rvContent.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContent.addItemDecoration(
GridSpacingItemDecoration(spanCount, spacingPx, true)
)
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
if (!recyclerView.canScrollVertically(1) && lastVisibleItemPosition == itemTotalCount) {
viewModel.loadAll(
isFree = if (isFree) true else null,
isPointAvailableOnly = if (isPointOnly) true else null
)
}
}
})
binding.rvContent.adapter = adapter
}
private fun bindData() {
viewModel.isLoading.observe(this) {
if (it) loadingDialog.show(screenWidth) else loadingDialog.dismiss()
}
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.themeListLiveData.observe(this) {
themeAdapter.addItems(it)
}
viewModel.itemsLiveData.observe(this) { list ->
if (adapter.itemCount > 0 || list.isNotEmpty()) {
binding.rvContent.visibility = View.VISIBLE
binding.llEmpty.visibility = View.GONE
} else {
binding.rvContent.visibility = View.GONE
binding.llEmpty.visibility = View.VISIBLE
}
adapter.appendItems(list)
}
viewModel.sortLiveData.observe(this) {
deselectSort()
selectSort(
when (it) {
AudioContentViewModel.Sort.POPULARITY -> {
binding.tvSortPopularity
}
else -> {
binding.tvSortNewest
}
}
)
}
}
private fun deselectSort() {
val color = ContextCompat.getColor(
applicationContext,
R.color.color_88e2e2e2
)
binding.tvSortNewest.setTextColor(color)
binding.tvSortPopularity.setTextColor(color)
}
private fun selectSort(view: TextView) {
view.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_e2e2e2
)
)
adapter.addItems(emptyList())
viewModel.loadAll(
isFree = if (isFree) true else null,
isPointAvailableOnly = if (isPointOnly) true else null
)
}
}

View File

@@ -0,0 +1,135 @@
package kr.co.vividnext.sodalive.audio_content.all
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.home.AudioContentMainItem
class AudioContentAllViewModel(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean> get() = _isLoading
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?> get() = _toastLiveData
private val _itemsLiveData = MutableLiveData<List<AudioContentMainItem>>()
val itemsLiveData: LiveData<List<AudioContentMainItem>> get() = _itemsLiveData
private var _themeListLiveData = MutableLiveData<List<String>>()
val themeListLiveData: LiveData<List<String>>
get() = _themeListLiveData
private var _sortLiveData = MutableLiveData(AudioContentViewModel.Sort.NEWEST)
val sortLiveData: LiveData<AudioContentViewModel.Sort>
get() = _sortLiveData
private var page = 1
private val size = 20
private var isLast = false
private var selectedTheme = "전체"
fun reset() {
page = 1
isLast = false
}
fun getThemeList(
isFree: Boolean? = null,
isPointAvailableOnly: Boolean? = null
) {
compositeDisposable.add(
repository.getAudioContentActiveThemeList(
isFree = isFree,
isPointAvailableOnly = isPointAvailableOnly,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val themeList = listOf("전체").union(it.data).toList()
_themeListLiveData.postValue(themeList)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun loadAll(
isFree: Boolean? = null,
isPointAvailableOnly: Boolean? = null
) {
if (_isLoading.value == true || isLast) return
_isLoading.value = true
compositeDisposable.add(
repository.getAllAudioContents(
page = page,
size = size,
isFree = isFree,
isPointAvailableOnly = isPointAvailableOnly,
sortType = _sortLiveData.value!!,
theme = if (selectedTheme == "전체") {
null
} else {
selectedTheme
},
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response ->
val list = response.data ?: emptyList()
if (list.isNotEmpty()) {
page += 1
}
if (list.size < size) {
isLast = true
}
_itemsLiveData.postValue(list)
_isLoading.value = false
}, { t ->
_isLoading.value = false
_toastLiveData.postValue(t.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
})
)
}
fun selectTheme(theme: String, isFree: Boolean, isPointOnly: Boolean) {
reset()
selectedTheme = theme
loadAll(isFree, isPointOnly)
}
fun selectSort(sortType: AudioContentViewModel.Sort) {
if (_sortLiveData.value != sortType) {
reset()
_sortLiveData.value = sortType
}
}
}

View File

@@ -12,7 +12,6 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
@@ -20,6 +19,7 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentNewAllBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.home.HomeContentThemeAdapter
import org.koin.android.ext.android.inject
@OptIn(UnstableApi::class)
@@ -30,7 +30,7 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
private lateinit var loadingDialog: LoadingDialog
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentThemeAdapter: HomeContentThemeAdapter
private lateinit var newContentAdapter: AudioContentNewAllAdapter
private var isFree: Boolean = false
@@ -65,7 +65,7 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
@SuppressLint("NotifyDataSetChanged")
private fun setupNewContentTheme() {
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
newContentThemeAdapter = HomeContentThemeAdapter {
newContentAdapter.clear()
newContentAdapter.notifyDataSetChanged()
viewModel.selectTheme(it, isFree = isFree)
@@ -109,10 +109,11 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
}
private fun setupNewContent() {
val spanCount = 3
val spacing = 40
// 아이템 정사각형 크기 계산: (screenWidth - (16*2) - 16) / 2
// 아이템 정사각형 크기 계산: (screenWidth - (paddingHorizontal*2) - itemSpacing) / 2
val itemSize = ((screenWidth - 16f.dpToPx() * 2 - 16f.dpToPx()) / 2f).toInt()
newContentAdapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
itemWidth = itemSize,
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
@@ -129,8 +130,12 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
}
)
val spanCount = 2
val spacingPx = 16f.dpToPx().toInt()
binding.rvContent.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContent.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
binding.rvContent.addItemDecoration(
GridSpacingItemDecoration(spanCount, spacingPx, true)
)
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {

View File

@@ -2,8 +2,6 @@ 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
@@ -15,12 +13,9 @@ 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(
@@ -47,11 +42,18 @@ class AudioContentNewAllAdapter(
)
.into(binding.ivAudioContentCoverImage)
val layoutParams = binding.ivAudioContentCoverImage.layoutParams as ConstraintLayout.LayoutParams
val layoutParams =
binding.ivAudioContentCoverImage.layoutParams as ConstraintLayout.LayoutParams
layoutParams.width = itemWidth
layoutParams.height = itemWidth
binding.ivAudioContentCoverImage.layoutParams = layoutParams
binding.ivPoint.visibility = if (item.isPointAvailable) {
View.VISIBLE
} else {
View.GONE
}
binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
@@ -94,7 +96,7 @@ class AudioContentNewAllAdapter(
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: AudioContentNewAllAdapter.ViewHolder, position: Int) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}

View File

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.audio_content.all
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
@@ -29,6 +30,12 @@ class AudioContentRankingAllAdapter(
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
binding.tvPoint.visibility = if (item.isPointAvailable) {
View.VISIBLE
} else {
View.GONE
}
binding.tvTitle.text = item.title
binding.tvRank.text = index.plus(1).toString()
binding.tvTheme.text = item.themeStr

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.audio_content.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kr.co.vividnext.sodalive.audio_content.PlaybackTracking
@Dao
interface PlaybackTrackingDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(entity: PlaybackTracking): Long
@Query("SELECT * FROM playback_tracking WHERE id = :id LIMIT 1")
fun getById(id: Long): PlaybackTracking?
@Query("SELECT * FROM playback_tracking")
fun getAll(): List<PlaybackTracking>
@Query("DELETE FROM playback_tracking")
fun deleteAll()
}

View File

@@ -0,0 +1,35 @@
package kr.co.vividnext.sodalive.audio_content.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import kr.co.vividnext.sodalive.audio_content.PlaybackTracking
import kr.co.vividnext.sodalive.common.Converter
@Database(entities = [PlaybackTracking::class], version = 1, exportSchema = true)
@TypeConverters(Converter::class)
abstract class PlaybackTrackingDatabase : RoomDatabase() {
abstract fun playbackTrackingDao(): PlaybackTrackingDao
companion object {
@Volatile
private var INSTANCE: PlaybackTrackingDatabase? = null
fun getDatabase(context: Context): PlaybackTrackingDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
PlaybackTrackingDatabase::class.java,
"playback_tracking_database"
)
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
INSTANCE = instance
instance
}
}
}
}

View File

@@ -1,8 +1,6 @@
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
@@ -56,6 +54,8 @@ 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.mypage.recent.RecentContentViewModel
import kr.co.vividnext.sodalive.mypage.recent.db.RecentContent
import kr.co.vividnext.sodalive.report.ReportType
import org.koin.android.ext.android.inject
import kotlin.math.ceil
@@ -65,6 +65,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
ActivityAudioContentDetailBinding::inflate
) {
private val viewModel: AudioContentDetailViewModel by inject()
private val recentContentViewModel: RecentContentViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var creatorOtherContentAdapter: OtherContentAdapter
@@ -105,7 +106,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
super.onCreate(savedInstanceState)
imm = getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager
imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
if (audioContentId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
@@ -115,7 +116,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
activityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
if (it.resultCode == RESULT_OK) {
contentOrder(audioContent, orderType)
}
}
@@ -129,7 +130,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
super.onResume()
val intentFilter = IntentFilter(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(audioContentReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
registerReceiver(audioContentReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(audioContentReceiver, intentFilter)
}
@@ -808,6 +809,15 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
)
}
)
recentContentViewModel.insertRecentContent(
RecentContent(
contentId = response.contentId,
coverImageUrl = response.coverImageUrl,
title = response.title,
creatorNickname = response.creator.nickname
)
)
}
binding.ivPlayOrPause.setImageResource(
@@ -1105,6 +1115,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
} else {
audioContent.price
},
isAvailableUsePoint = binding.ivPoint.visibility == View.VISIBLE,
confirmButtonClick = {
startService(
Intent(this, AudioContentPlayService::class.java).apply {
@@ -1187,7 +1198,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
false
)
viewModel.isLoading.value = isLoading ?: false
viewModel.isLoading.value = isLoading == true
if (this@AudioContentDetailActivity.audioContentId == contentId) {
runOnUiThread {

View File

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

View File

@@ -1,737 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllActivity
import kr.co.vividnext.sodalive.audio_content.all.AudioContentRankingAllActivity
import kr.co.vividnext.sodalive.audio_content.all.by_theme.AudioContentAllByThemeActivity
import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity
import kr.co.vividnext.sodalive.audio_content.curation.AudioContentCurationActivity
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerViewModel
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationAdapter
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationViewModel
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentViewModel
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainCreatorRankingViewModel
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingAdapter
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingViewModel
import kr.co.vividnext.sodalive.audio_content.main.recommend_series.AudioContentMainRecommendSeriesViewModel
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainBinding
import kr.co.vividnext.sodalive.explorer.ExplorerSectionAdapter
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.explorer.profile.series.UserProfileSeriesListAdapter
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.mypage.alarm.AlarmListActivity
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.notification.MemberRole
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
@OptIn(UnstableApi::class)
class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
FragmentAudioContentMainBinding::inflate
) {
private val creatorRankViewModel: AudioContentMainCreatorRankingViewModel by inject()
private lateinit var creatorRankAdaptor: ExplorerSectionAdapter
private val recommendSeriesViewModel: AudioContentMainRecommendSeriesViewModel by inject()
private lateinit var seriesAdapter: UserProfileSeriesListAdapter
private val bannerViewModel: AudioContentMainBannerViewModel by inject()
private lateinit var bannerAdapter: AudioContentMainBannerAdapter
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)
setupView()
curationViewModel.getCurationList()
bannerViewModel.getMainBannerList()
newContentViewModel.getThemeList()
creatorRankViewModel.getCreatorRank()
newContentViewModel.getNewContentOfTheme("전체")
contentRankingViewModel.getContentRanking()
contentRankingViewModel.getContentRankingSortType()
recommendSeriesViewModel.getRecommendSeriesList()
}
private fun setupView() {
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
binding.llUploadContent.visibility = View.VISIBLE
binding.llUploadContent.setOnClickListener {
startActivity(
Intent(
requireActivity(),
AudioContentUploadActivity::class.java
)
)
}
} else {
binding.llUploadContent.visibility = View.GONE
}
setupCreatorRank()
setupRecommendSeries()
setupBanner()
setupNewContentTheme()
setupNewContent()
setupContentRankingSortType()
setupContentRanking()
setupCuration()
binding.llShortPlay.setOnClickListener {
startActivity(
Intent(requireContext(), AudioContentAllByThemeActivity::class.java).apply {
putExtra(Constants.EXTRA_THEME_ID, 11L)
}
)
}
binding.llMorningCall.setOnClickListener {
startActivity(
Intent(requireContext(), AudioContentAllByThemeActivity::class.java).apply {
putExtra(Constants.EXTRA_THEME_ID, 12L)
}
)
}
binding.ivContentKeep.setOnClickListener {
startActivity(
Intent(
requireContext(),
AudioContentBoxActivity::class.java
)
)
}
binding.ivAlarm.setOnClickListener {
startActivity(
Intent(
requireActivity(),
AlarmListActivity::class.java
)
)
}
binding.flSearchChannel.setOnClickListener {
}
}
private fun setupCreatorRank() {
creatorRankAdaptor = ExplorerSectionAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
},
isVisibleRanking = true
)
binding.rvCreatorRank.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvCreatorRank.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
creatorRankAdaptor.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvCreatorRank.adapter = creatorRankAdaptor
creatorRankViewModel.creatorRankLiveData.observe(viewLifecycleOwner) {
binding.tvDesc.text = it.desc
binding.tvCreatorRankTitle.text = if (
!it.coloredTitle.isNullOrBlank() &&
!it.color.isNullOrBlank()
) {
val spStr = SpannableString(it.title)
try {
spStr.setSpan(
ForegroundColorSpan(
Color.parseColor("#${it.color}")
),
it.title.indexOf(it.coloredTitle),
it.title.indexOf(it.coloredTitle) + it.coloredTitle.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
spStr
} catch (e: IllegalArgumentException) {
it.title
}
} else {
it.title
}
creatorRankAdaptor.addItems(it.creators)
if (creatorRankAdaptor.itemCount <= 0 && it.creators.isEmpty()) {
binding.llCreatorRank.visibility = View.GONE
binding.rvCreatorRank.visibility = View.GONE
} else {
binding.llCreatorRank.visibility = View.VISIBLE
binding.rvCreatorRank.visibility = View.VISIBLE
}
}
creatorRankViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupRecommendSeries() {
seriesAdapter = UserProfileSeriesListAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
},
isVisibleCreator = true
)
val recyclerView = binding.rvRecommendSeries
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
seriesAdapter.itemCount - 1 -> {
outRect.right = 0
outRect.left = 6.7f.dpToPx().toInt()
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = seriesAdapter
recommendSeriesViewModel.seriesListLiveData.observe(viewLifecycleOwner) {
seriesAdapter.addItems(it)
binding.llRecommendSeries.visibility = if (
seriesAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
recommendSeriesViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
binding.llRecommendSeriesRefresh.setOnClickListener {
seriesAdapter.clear()
recommendSeriesViewModel.getRecommendSeriesList()
}
}
private fun setupBanner() {
val layoutParams = binding
.rvBanner
.layoutParams as LinearLayout.LayoutParams
val pagerWidth = screenWidth.toDouble() - 26.7f.dpToPx()
val pagerHeight = (pagerWidth * 0.53).roundToInt()
layoutParams.width = pagerWidth.roundToInt()
layoutParams.height = pagerHeight
bannerAdapter = AudioContentMainBannerAdapter(
requireContext(),
pagerWidth.roundToInt(),
pagerHeight
) {
when (it.type) {
AudioContentBannerType.EVENT -> {
startActivity(
Intent(requireContext(), EventDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_EVENT, it.eventItem!!)
}
)
}
AudioContentBannerType.CREATOR -> {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it.creatorId!!)
}
)
}
AudioContentBannerType.SERIES -> {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it.seriesId!!)
}
)
}
AudioContentBannerType.LINK -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
}
}
}
binding
.rvBanner
.layoutParams = layoutParams
binding.rvBanner.apply {
adapter = bannerAdapter as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(1000)
setInterval(4 * 1000)
}.create()
binding
.rvBanner
.setIndicatorView(binding.indicatorBanner)
.setIndicatorStyle(IndicatorStyle.ROUND_RECT)
.setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
.setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_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 setupNewContentTheme() {
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
newContentViewModel.getNewContentOfTheme(theme = it)
}
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContentTheme.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
newContentThemeAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvNewContentTheme.adapter = newContentThemeAdapter
newContentViewModel.themeListLiveData.observe(viewLifecycleOwner) {
binding.llNewContent.visibility = View.VISIBLE
newContentThemeAdapter.addItems(it)
}
}
private fun setupNewContent() {
binding.ivNewContentAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentNewAllActivity::class.java))
}
newContentAdapter = AudioContentMainContentAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvNewContent.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
newContentAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvNewContent.adapter = newContentAdapter
newContentViewModel.newContentListLiveData.observe(viewLifecycleOwner) {
newContentAdapter.addItems(it)
}
newContentViewModel.isLoading.observe(viewLifecycleOwner) {
binding.pbNewContent.visibility = if (it) {
View.VISIBLE
} else {
View.GONE
}
}
newContentViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupContentRankingSortType() {
contentRankingSortAdapter = AudioContentMainNewContentThemeAdapter {
contentRankingViewModel.getContentRanking(sort = it)
}
binding.rvContentRankingSort.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvContentRankingSort.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
contentRankingSortAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvContentRankingSort.adapter = contentRankingSortAdapter
contentRankingViewModel.contentRankingSortListLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
contentRankingSortAdapter.addItems(it)
}
}
@SuppressLint("SetTextI18n")
private fun setupContentRanking() {
binding.ivContentRankingAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentRankingAllActivity::class.java))
}
contentRankingAdapter = AudioContentMainRankingAdapter(
width = (screenWidth * 0.66).toInt()
) {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
}
binding.rvContentRanking.layoutManager = GridLayoutManager(
context,
3,
GridLayoutManager.HORIZONTAL,
false
)
binding.rvContentRanking.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
}
})
binding.rvContentRanking.adapter = contentRankingAdapter
contentRankingViewModel.contentRankingLiveData.observe(viewLifecycleOwner) {
binding.llContentRanking.visibility = View.VISIBLE
binding.tvDate.text = "${it.startDate}~${it.endDate}"
contentRankingAdapter.addItems(it.items)
}
contentRankingViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
private fun setupCuration() {
curationAdapter = AudioContentMainCurationAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
},
onClickCurationMore = { curationId, title ->
startActivity(
Intent(requireContext(), AudioContentCurationActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_ID, curationId)
putExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_TITLE, title)
}
)
}
)
binding.rvCuration.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
binding.rvCuration.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 40f.dpToPx().toInt()
outRect.bottom = 20f.dpToPx().toInt()
}
curationAdapter.itemCount - 1 -> {
outRect.top = 20f.dpToPx().toInt()
outRect.bottom = 40f.dpToPx().toInt()
}
else -> {
outRect.top = 20f.dpToPx().toInt()
outRect.bottom = 20f.dpToPx().toInt()
}
}
}
})
binding.rvCuration.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
curationViewModel.curationListLiveData.observe(viewLifecycleOwner) {
if (curationViewModel.page == 2) {
curationAdapter.clear()
}
curationAdapter.addItems(it)
binding.rvCuration.visibility = if (curationAdapter.itemCount <= 0 && it.isEmpty()) {
View.GONE
} else {
View.VISIBLE
}
}
curationViewModel.isLoading.observe(viewLifecycleOwner) {
binding.pbCuration.visibility = if (it) {
View.VISIBLE
} else {
View.GONE
}
}
curationViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
}

View File

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

View File

@@ -4,13 +4,6 @@ import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.settings.event.EventItem
@Keep
data class ContentCreatorResponse(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
)
@Keep
data class GetAudioContentMainItem(
@SerializedName("contentId") val contentId: Long,
@@ -20,7 +13,8 @@ data class GetAudioContentMainItem(
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("price") val price: Int,
@SerializedName("duration") val duration: String
@SerializedName("duration") val duration: String,
@SerializedName("isPointAvailable") val isPointAvailable: Boolean
)
@Keep
@@ -40,6 +34,7 @@ data class GetAudioContentRankingItem(
@SerializedName("duration") val duration: String,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("isPointAvailable") val isPointAvailable: Boolean,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
)

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,162 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.alarm.all
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAlarmContentAllBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
@OptIn(UnstableApi::class)
class AlarmContentAllActivity : BaseActivity<ActivityAlarmContentAllBinding>(
ActivityAlarmContentAllBinding::inflate
) {
private val viewModel: AlarmContentAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentAdapter: AudioContentNewAllAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.getContentMainAlarmAll()
newContentThemeAdapter.addItems(listOf("전체", "모닝콜", "슬립콜", "알람"))
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "새로운 알람"
binding.toolbar.tvBack.setOnClickListener { finish() }
setupNewContentTheme()
setupNewContent()
}
private fun setupNewContentTheme() {
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
newContentAdapter.clear()
viewModel.selectTheme(it)
}
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContentTheme.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
newContentThemeAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvNewContentTheme.adapter = newContentThemeAdapter
}
private fun setupNewContent() {
val spanCount = 3
val spacing = 40
newContentAdapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(this, UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvContent.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContent.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getContentMainAlarmAll()
}
}
})
binding.rvContent.adapter = newContentAdapter
}
private fun bindData() {
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.newContentListLiveData.observe(this) {
newContentAdapter.addItems(it)
}
viewModel.totalCountLiveData.observe(this) {
binding.tvTotalCount.text = "$it"
}
}
}

View File

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

View File

@@ -1,116 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.asmr
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllViewModel
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAsmrNewContentAllBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import org.koin.android.ext.android.inject
@OptIn(UnstableApi::class)
class AsmrNewContentAllActivity : BaseActivity<ActivityAsmrNewContentAllBinding>(
ActivityAsmrNewContentAllBinding::inflate
) {
private val viewModel: AudioContentNewAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var newContentAdapter: AudioContentNewAllAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.selectTheme(theme = "ASMR", isFree = false)
}
@SuppressLint("SetTextI18n")
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "새로운 ASMR"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.tvNotice.text = "※ 최근 2주간 등록된 새로운 ASMR 입니다."
setupNewContent()
}
private fun setupNewContent() {
val spanCount = 3
val spacing = 40
newContentAdapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(this, UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvContent.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContent.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getNewContentList()
}
}
})
binding.rvContent.adapter = newContentAdapter
}
private fun bindData() {
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.newContentListLiveData.observe(this) {
newContentAdapter.addItems(it)
}
viewModel.newContentTotalCountLiveData.observe(this) {
binding.tvTotalCount.text = "$it"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,104 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.free.introduce_creator
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityIntroduceCreatorBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
@OptIn(UnstableApi::class)
class IntroduceCreatorActivity : BaseActivity<ActivityIntroduceCreatorBinding>(
ActivityIntroduceCreatorBinding::inflate
) {
private val viewModel: IntroduceCreatorViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentNewAllAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.getIntroduceCreatorList()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "크리에이터 소개"
binding.toolbar.tvBack.setOnClickListener { finish() }
val spanCount = 3
val spacing = 13.3f.dpToPx().roundToInt()
adapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(this, UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvContent.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContent.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getIntroduceCreatorList()
}
}
})
binding.rvContent.adapter = adapter
}
private fun bindData() {
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.introduceCreatorListLiveData.observe(this) {
adapter.addItems(it)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,114 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.replay
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllViewModel
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAsmrNewContentAllBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import org.koin.android.ext.android.inject
@OptIn(UnstableApi::class)
class ReplayNewContentAllActivity : BaseActivity<ActivityAsmrNewContentAllBinding>(
ActivityAsmrNewContentAllBinding::inflate
) {
private val viewModel: AudioContentNewAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var newContentAdapter: AudioContentNewAllAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.selectTheme(theme = "다시듣기", isFree = false)
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "새로운 라이브 다시듣기"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.tvNotice.text = "※ 최근 2주간 등록된 새로운 라이브 다시듣기 입니다."
setupNewContent()
}
private fun setupNewContent() {
val spanCount = 3
val spacing = 40
newContentAdapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(this, UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvContent.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContent.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getNewContentList()
}
}
})
binding.rvContent.adapter = newContentAdapter
}
private fun bindData() {
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.newContentListLiveData.observe(this) {
newContentAdapter.addItems(it)
}
viewModel.newContentTotalCountLiveData.observe(this) {
binding.tvTotalCount.text = "$it"
}
}
}

View File

@@ -1,787 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.series
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.ContentRankCreatorAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.series.completed.CompletedSeriesActivity
import kr.co.vividnext.sodalive.audio_content.main.v2.series.curation.AudioContentMainSeriesCurationAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.series.new_series.AudioContentMainNewSeriesAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.series.origianl_audio_drama.AudioContentMainTabSeriesOriginalAudioDramaAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.series.origianl_audio_drama.OriginalAudioDramaContentAllActivity
import kr.co.vividnext.sodalive.audio_content.main.v2.series.rank_series.AudioContentMainSeriesRankingAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.series.recommend_by_genre.AudioContentMainRecommendSeriesGenreAdapter
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainTabSeriesBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.explorer.profile.series.UserProfileSeriesListAdapter
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.event_banner.EventBannerAdapter
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
class AudioContentMainTabSeriesFragment : BaseFragment<FragmentAudioContentMainTabSeriesBinding>(
FragmentAudioContentMainTabSeriesBinding::inflate
) {
private val viewModel: AudioContentMainTabSeriesViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var contentBannerAdapter: AudioContentMainBannerAdapter
private lateinit var audioDramaAdapter: AudioContentMainTabSeriesOriginalAudioDramaAdapter
private lateinit var rankDailySeriesAdapter: AudioContentMainSeriesRankingAdapter
private lateinit var seriesGenreAdapter: AudioContentMainRecommendSeriesGenreAdapter
private lateinit var recommendSeriesByGenreAdapter: UserProfileSeriesListAdapter
private lateinit var newSeriesAdapter: AudioContentMainNewSeriesAdapter
private lateinit var completedSeriesAdapter: UserProfileSeriesListAdapter
private lateinit var recommendSeriesCreatorAdapter: ContentRankCreatorAdapter
private lateinit var recommendSeriesByChannelAdapter: UserProfileSeriesListAdapter
private lateinit var curationAdapter: AudioContentMainSeriesCurationAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
bindData()
viewModel.fetchData()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupContentBanner()
setupOriginalAudioDrama()
setupRankSeries()
setupRecommendSeriesGenre()
setupRecommendSeriesByGenre()
setupNewSeries()
setupCompleteSeries()
setupRecommendSeriesByChannelCreator()
setupRecommendSeriesByChannel()
setupEventBanner()
setupCuration()
}
private fun setupContentBanner() {
val layoutParams = binding
.rvBanner
.layoutParams as LinearLayout.LayoutParams
val pagerWidth = screenWidth.toDouble() - 26.7f.dpToPx()
val pagerHeight = (pagerWidth * 0.53).roundToInt()
layoutParams.width = pagerWidth.roundToInt()
layoutParams.height = pagerHeight
contentBannerAdapter = AudioContentMainBannerAdapter(
requireContext(),
pagerWidth.roundToInt(),
pagerHeight
) {
when (it.type) {
AudioContentBannerType.EVENT -> {
startActivity(
Intent(requireContext(), EventDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_EVENT, it.eventItem!!)
}
)
}
AudioContentBannerType.CREATOR -> {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it.creatorId!!)
}
)
}
AudioContentBannerType.SERIES -> {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it.seriesId!!)
}
)
}
AudioContentBannerType.LINK -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
}
}
}
binding
.rvBanner
.layoutParams = layoutParams
binding.rvBanner.apply {
adapter = contentBannerAdapter as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(1000)
setInterval(4 * 1000)
}.create()
binding
.rvBanner
.setIndicatorView(binding.indicatorBanner)
.setIndicatorStyle(IndicatorStyle.ROUND_RECT)
.setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
.setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_3bb9f1)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
viewModel.contentBannerLiveData.observe(viewLifecycleOwner) {
if (contentBannerAdapter.itemCount <= 0 && it.isEmpty()) {
binding.rvBanner.visibility = View.GONE
binding.indicatorBanner.visibility = View.GONE
} else {
binding.rvBanner.visibility = View.VISIBLE
binding.indicatorBanner.visibility = View.VISIBLE
binding.rvBanner.refreshData(it)
}
}
}
private fun setupOriginalAudioDrama() {
audioDramaAdapter = AudioContentMainTabSeriesOriginalAudioDramaAdapter {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
}
val recyclerView = binding.rvOriginalAudio
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
audioDramaAdapter.itemCount - 1 -> {
outRect.right = 0
outRect.left = 6.7f.dpToPx().toInt()
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = audioDramaAdapter
viewModel.originalAudioDramaLiveData.observe(viewLifecycleOwner) {
audioDramaAdapter.addItems(it)
binding.llOriginalAudioDrama.visibility =
if (audioDramaAdapter.isVisibleRecyclerView()) {
View.VISIBLE
} else {
View.GONE
}
}
binding.ivOriginalAudioDramaAll.setOnClickListener {
startActivity(
Intent(
requireContext(),
OriginalAudioDramaContentAllActivity::class.java
)
)
}
}
private fun setupRankSeries() {
rankDailySeriesAdapter = AudioContentMainSeriesRankingAdapter {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
}
val recyclerView = binding.rvRankSeries
recyclerView.layoutManager = GridLayoutManager(
context,
3,
GridLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
})
recyclerView.adapter = rankDailySeriesAdapter
viewModel.rankSeriesListLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llRankSeries.visibility = View.VISIBLE
rankDailySeriesAdapter.addItems(it)
} else {
binding.llRankSeries.visibility = View.GONE
}
}
}
private fun setupRecommendSeriesGenre() {
seriesGenreAdapter = AudioContentMainRecommendSeriesGenreAdapter {
binding.llNoItemsSeriesByGenre.visibility = View.VISIBLE
binding.rvSeriesByGenre.visibility = View.GONE
viewModel.selectGenre(it)
}
val recyclerView = binding.rvSeriesGenre
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
seriesGenreAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = seriesGenreAdapter
viewModel.genreListLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llSeriesByGenre.visibility = View.VISIBLE
seriesGenreAdapter.addItems(it)
} else {
binding.llSeriesByGenre.visibility = View.GONE
}
}
}
private fun setupRecommendSeriesByGenre() {
recommendSeriesByGenreAdapter = UserProfileSeriesListAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
},
isVisibleCreator = true
)
val recyclerView = binding.rvSeriesByGenre
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
recommendSeriesByGenreAdapter.itemCount - 1 -> {
outRect.right = 0
outRect.left = 6.7f.dpToPx().toInt()
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = recommendSeriesByGenreAdapter
viewModel.recommendSeriesListLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llNoItemsSeriesByGenre.visibility = View.GONE
binding.rvSeriesByGenre.visibility = View.VISIBLE
recommendSeriesByGenreAdapter.clear()
recommendSeriesByGenreAdapter.addItems(it)
}
}
}
private fun setupNewSeries() {
newSeriesAdapter = AudioContentMainNewSeriesAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
val recyclerView = binding.rvNewSeries
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
recommendSeriesByGenreAdapter.itemCount - 1 -> {
outRect.right = 0
outRect.left = 6.7f.dpToPx().toInt()
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = newSeriesAdapter
viewModel.newSeriesListLiveData.observe(viewLifecycleOwner) {
newSeriesAdapter.addItems(it)
binding.llNewSeries.visibility = if (it.isNotEmpty()) {
View.VISIBLE
} else {
View.GONE
}
}
}
private fun setupCompleteSeries() {
binding.ivCompleteSeriesAll.setOnClickListener {
startActivity(
Intent(
requireContext(),
CompletedSeriesActivity::class.java
)
)
}
completedSeriesAdapter = UserProfileSeriesListAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
},
isVisibleCreator = true
)
val recyclerView = binding.rvCompleteSeries
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
completedSeriesAdapter.itemCount - 1 -> {
outRect.right = 0
outRect.left = 6.7f.dpToPx().toInt()
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = completedSeriesAdapter
viewModel.rankCompleteSeriesListLiveData.observe(viewLifecycleOwner) {
completedSeriesAdapter.addItems(it)
binding.llCompleteSeries.visibility = if (
completedSeriesAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
}
private fun setupRecommendSeriesByChannelCreator() {
recommendSeriesCreatorAdapter = ContentRankCreatorAdapter {
binding.llNoItems.visibility = View.VISIBLE
binding.rvRecommendSeriesByChannel.visibility = View.GONE
viewModel.getRecommendSeriesByCreator(it)
}
val recyclerView = binding.rvRecommendSeriesChannel
recyclerView.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 11f.dpToPx().toInt()
}
recommendSeriesCreatorAdapter.itemCount - 1 -> {
outRect.left = 11f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 11f.dpToPx().toInt()
outRect.right = 11f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = recommendSeriesCreatorAdapter
viewModel.seriesRankCreatorListLiveData.observe(viewLifecycleOwner) {
recommendSeriesCreatorAdapter.addItems(it)
if (recommendSeriesCreatorAdapter.itemCount <= 0 && it.isEmpty()) {
binding.llRecommendSeriesByChannel.visibility = View.GONE
} else {
binding.llRecommendSeriesByChannel.visibility = View.VISIBLE
}
}
}
private fun setupRecommendSeriesByChannel() {
recommendSeriesByChannelAdapter = UserProfileSeriesListAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
},
isVisibleCreator = true
)
val recyclerView = binding.rvRecommendSeriesByChannel
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
recommendSeriesByChannelAdapter.itemCount - 1 -> {
outRect.right = 0
outRect.left = 6.7f.dpToPx().toInt()
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = recommendSeriesByChannelAdapter
viewModel.recommendSeriesByChannelLiveData.observe(viewLifecycleOwner) {
recommendSeriesByChannelAdapter.clear()
recommendSeriesByChannelAdapter.addItems(it)
if (recommendSeriesCreatorAdapter.itemCount <= 0) {
binding.llNoItems.visibility = View.VISIBLE
binding.rvRecommendSeriesByChannel.visibility = View.GONE
} else {
binding.llNoItems.visibility = View.GONE
binding.rvRecommendSeriesByChannel.visibility = View.VISIBLE
}
}
}
private fun setupEventBanner() {
val imageSliderLp = binding.eventBannerSlider.layoutParams
imageSliderLp.width = screenWidth
imageSliderLp.height = (screenWidth * 300) / 1000
binding.eventBannerSlider.layoutParams = imageSliderLp
binding.eventBannerSlider.apply {
adapter = EventBannerAdapter(requireContext()) {
if (it.detailImageUrl != null) {
val intent = Intent(requireActivity(), EventDetailActivity::class.java)
intent.putExtra(Constants.EXTRA_EVENT, it)
startActivity(intent)
} else if (!it.link.isNullOrBlank()) {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(it.link)
)
)
}
} as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(800)
}.create()
binding.eventBannerSlider
.setIndicatorView(binding.indicatorEventBanner)
.setIndicatorStyle(IndicatorStyle.ROUND_RECT)
.setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
.setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_3bb9f1)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
viewModel.eventLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.eventBannerSlider.visibility = View.VISIBLE
binding.indicatorEventBanner.visibility = View.VISIBLE
binding.eventBannerSlider.refreshData(it)
} else {
binding.eventBannerSlider.visibility = View.GONE
binding.indicatorEventBanner.visibility = View.GONE
}
}
}
private fun setupCuration() {
curationAdapter = AudioContentMainSeriesCurationAdapter(
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)
}
)
},
)
binding.rvCuration.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
binding.rvCuration.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 30f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
curationAdapter.itemCount - 1 -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 30f.dpToPx().toInt()
}
else -> {
outRect.top = 15f.dpToPx().toInt()
outRect.bottom = 15f.dpToPx().toInt()
}
}
}
})
binding.rvCuration.adapter = curationAdapter
viewModel.curationListLiveData.observe(viewLifecycleOwner) {
curationAdapter.addItems(it)
binding.rvCuration.visibility = if (curationAdapter.itemCount <= 0 && it.isEmpty()) {
View.GONE
} else {
View.VISIBLE
}
}
}
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
}
}

View File

@@ -1,45 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.series
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.ContentType
class AudioContentMainTabSeriesRepository(private val api: AudioContentApi) {
fun getContentMainSeries(token: String) = api.getContentMainSeries(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getRecommendSeriesListByGenre(
genreId: Long,
token: String
) = api.getRecommendSeriesListByGenre(
genreId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getRecommendSeriesByCreator(
creatorId: Long,
token: String
) = api.getRecommendSeriesByCreator(
creatorId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getCompletedSeries(
page: Int,
size: Int,
token: String
) = api.getCompletedSeries(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
authHeader = token
)
}

View File

@@ -1,183 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.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.main.ContentCreatorResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.event.EventItem
class AudioContentMainTabSeriesViewModel(
private val repository: AudioContentMainTabSeriesRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _contentBannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
val contentBannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
get() = _contentBannerLiveData
private var _originalAudioDramaLiveData =
MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val originalAudioDramaLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _originalAudioDramaLiveData
private var _rankSeriesListLiveData =
MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val rankSeriesListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _rankSeriesListLiveData
private var _genreListLiveData = MutableLiveData<List<GetSeriesGenreListResponse>>()
val genreListLiveData: LiveData<List<GetSeriesGenreListResponse>>
get() = _genreListLiveData
private var _recommendSeriesListLiveData =
MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val recommendSeriesListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _recommendSeriesListLiveData
private var _newSeriesListLiveData = MutableLiveData<List<GetRecommendSeriesListResponse>>()
val newSeriesListLiveData: LiveData<List<GetRecommendSeriesListResponse>>
get() = _newSeriesListLiveData
private var _rankCompleteSeriesListLiveData =
MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val rankCompleteSeriesListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _rankCompleteSeriesListLiveData
private var _seriesRankCreatorListLiveData = MutableLiveData<List<ContentCreatorResponse>>()
val seriesRankCreatorListLiveData: LiveData<List<ContentCreatorResponse>>
get() = _seriesRankCreatorListLiveData
private var _recommendSeriesByChannelLiveData =
MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val recommendSeriesByChannelLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _recommendSeriesByChannelLiveData
private val _eventLiveData = MutableLiveData<List<EventItem>>()
val eventLiveData: LiveData<List<EventItem>>
get() = _eventLiveData
private val _curationListLiveData = MutableLiveData<List<GetSeriesCurationResponse>>()
val curationListLiveData: LiveData<List<GetSeriesCurationResponse>>
get() = _curationListLiveData
fun fetchData() {
_isLoading.value = true
compositeDisposable.add(
repository.getContentMainSeries(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val data = it.data
_contentBannerLiveData.value = data.contentBannerList
_originalAudioDramaLiveData.value = data.originalAudioDrama
_rankSeriesListLiveData.value = data.rankSeriesList
_genreListLiveData.value = data.genreList
_recommendSeriesListLiveData.value = data.recommendSeriesList
_newSeriesListLiveData.value = data.newSeriesList
_rankCompleteSeriesListLiveData.value = data.rankCompleteSeriesList
_seriesRankCreatorListLiveData.value = data.seriesRankCreatorList
_recommendSeriesByChannelLiveData.value = data.recommendSeriesByChannel
_eventLiveData.value = data.eventBannerList.eventList
_curationListLiveData.value = data.curationList
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun selectGenre(genreId: Long) {
_isLoading.value = true
compositeDisposable.add(
repository.getRecommendSeriesListByGenre(
genreId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_recommendSeriesListLiveData.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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun getRecommendSeriesByCreator(creatorId: Long) {
_isLoading.value = true
compositeDisposable.add(
repository.getRecommendSeriesByCreator(creatorId, token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_recommendSeriesByChannelLiveData.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,34 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.series
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.ContentCreatorResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.settings.event.GetEventResponse
@Keep
data class GetContentMainTabSeriesResponse(
@SerializedName("contentBannerList")
val contentBannerList: List<GetAudioContentBannerResponse>,
@SerializedName("originalAudioDrama")
val originalAudioDrama: List<GetSeriesListResponse.SeriesListItem>,
@SerializedName("rankSeriesList")
val rankSeriesList: List<GetSeriesListResponse.SeriesListItem>,
@SerializedName("genreList")
val genreList: List<GetSeriesGenreListResponse>,
@SerializedName("recommendSeriesList")
val recommendSeriesList: List<GetSeriesListResponse.SeriesListItem>,
@SerializedName("newSeriesList")
val newSeriesList: List<GetRecommendSeriesListResponse>,
@SerializedName("rankCompleteSeriesList")
val rankCompleteSeriesList: List<GetSeriesListResponse.SeriesListItem>,
@SerializedName("seriesRankCreatorList")
val seriesRankCreatorList: List<ContentCreatorResponse>,
@SerializedName("recommendSeriesByChannel")
val recommendSeriesByChannel: List<GetSeriesListResponse.SeriesListItem>,
@SerializedName("eventBannerList")
val eventBannerList: GetEventResponse,
@SerializedName("curationList")
val curationList: List<GetSeriesCurationResponse>
)

View File

@@ -1,14 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.series
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class GetRecommendSeriesListResponse(
@SerializedName("seriesId") val seriesId: Long,
@SerializedName("title") val title: String,
@SerializedName("imageUrl") val imageUrl: String,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
)

View File

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

View File

@@ -1,109 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.series.completed
import android.content.Intent
import android.os.Bundle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAdapter
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.ActivityCompletedSeriesBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
class CompletedSeriesActivity : BaseActivity<ActivityCompletedSeriesBinding>(
ActivityCompletedSeriesBinding::inflate
) {
private val viewModel: CompletedSeriesViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: SeriesListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.getCompletedSeries()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "완결 시리즈"
binding.toolbar.tvBack.setOnClickListener { finish() }
setupCompletedSeriesListView()
}
private fun setupCompletedSeriesListView() {
val spacing = 13.3f.dpToPx().roundToInt()
adapter = SeriesListAdapter(
itemWidth = ((screenWidth - spacing * 4) / 3f).roundToInt(),
onClickItem = {
startActivity(
Intent(applicationContext, SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
},
onClickCreator = {},
isVisibleCreator = true
)
val spanCount = 3
val recyclerView = binding.rvSeries
recyclerView.layoutManager = GridLayoutManager(this, spanCount)
recyclerView.addItemDecoration(
DifferentSpacingItemDecoration(
spanCount = spanCount,
horizontalSpacing = spacing,
verticalSpacing = spacing,
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.getCompletedSeries()
}
}
})
recyclerView.adapter = adapter
}
private fun bindData() {
viewModel.toastLiveData.observe(this) {
it?.let { showToast(it) }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.completedSeriesLiveData.observe(this) {
adapter.addItems(it)
}
}
}

View File

@@ -1,75 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.series.completed
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.main.v2.series.AudioContentMainTabSeriesRepository
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class CompletedSeriesViewModel(
private val repository: AudioContentMainTabSeriesRepository
) : 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 _completedSeriesLiveData =
MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val completedSeriesLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _completedSeriesLiveData
var isLast = false
var page = 1
private val size = 20
fun getCompletedSeries() {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getCompletedSeries(
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()) {
_completedSeriesLiveData.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

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

View File

@@ -1,77 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.series.new_series
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.v2.series.GetRecommendSeriesListResponse
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainNewSeriesBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainNewSeriesAdapter(
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentMainNewSeriesAdapter.ViewHolder>() {
private val items = mutableListOf<GetRecommendSeriesListResponse>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentMainNewSeriesBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetRecommendSeriesListResponse) {
Glide
.with(context)
.load(item.imageUrl)
.apply(
RequestOptions().transform(
CenterCrop(),
RoundedCorners(5f.dpToPx().toInt())
)
)
.into(binding.ivCover)
binding.ivCreatorProfile.load(item.creatorProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvTitle.text = item.title
binding.tvCreatorNickname.text = item.creatorNickname
binding.root.setOnClickListener { onClickItem(item.seriesId) }
binding.tvTitle.setOnClickListener { onClickCreator(item.creatorId) }
binding.tvCreatorNickname.setOnClickListener { onClickCreator(item.creatorId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemAudioContentMainNewSeriesBinding.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<GetRecommendSeriesListResponse>) {
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@@ -1,109 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.series.origianl_audio_drama
import android.content.Intent
import android.os.Bundle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAdapter
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.ActivityOriginalAudioDramaContentAllBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
class OriginalAudioDramaContentAllActivity :
BaseActivity<ActivityOriginalAudioDramaContentAllBinding>(
ActivityOriginalAudioDramaContentAllBinding::inflate
) {
private val viewModel: OriginalAudioDramaContentAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: SeriesListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.getOriginalAudioDramaList()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "오리지널 오디오 드라마"
binding.toolbar.tvBack.setOnClickListener { finish() }
setupOriginalAudioDramaListView()
}
private fun setupOriginalAudioDramaListView() {
val spacing = 13.3f.dpToPx().roundToInt()
adapter = SeriesListAdapter(
itemWidth = ((screenWidth - spacing * 3) / 2f).roundToInt(),
onClickItem = {
startActivity(
Intent(applicationContext, SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
},
onClickCreator = {},
isVisibleCreator = false
)
val spanCount = 2
val recyclerView = binding.rvSeries
recyclerView.layoutManager = GridLayoutManager(this, spanCount)
recyclerView.addItemDecoration(
DifferentSpacingItemDecoration(
spanCount = spanCount,
horizontalSpacing = spacing,
verticalSpacing = spacing,
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.getOriginalAudioDramaList()
}
}
})
recyclerView.adapter = adapter
}
private fun bindData() {
viewModel.toastLiveData.observe(this) {
it?.let { showToast(it) }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.originalAudioDramaLiveData.observe(this) {
adapter.addItems(it)
}
}
}

View File

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

View File

@@ -1,74 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.series.origianl_audio_drama
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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class OriginalAudioDramaContentAllViewModel(
private val repository: OriginalAudioDramaContentAllRepository
) : 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 _originalAudioDramaLiveData =
MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val originalAudioDramaLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _originalAudioDramaLiveData
var isLast = false
var page = 1
private val size = 15
fun getOriginalAudioDramaList() {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getOriginalAudioDramaList(
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()) {
_originalAudioDramaLiveData.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

@@ -1,57 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.series.rank_series
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainSeriesRankingBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainSeriesRankingAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentMainSeriesRankingAdapter.ViewHolder>() {
val items = mutableListOf<GetSeriesListResponse.SeriesListItem>()
inner class ViewHolder(
private val binding: ItemAudioContentMainSeriesRankingBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(item: GetSeriesListResponse.SeriesListItem, index: Int) {
binding.root.setOnClickListener { onClickItem(item.seriesId) }
binding.tvTitle.text = item.title
binding.tvRank.text = "${index + 1}"
binding.tvNickname.text = item.creator.nickname
binding.ivCover.load(item.coverImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5f.dpToPx()))
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAudioContentMainSeriesRankingBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], index = position)
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetSeriesListResponse.SeriesListItem>) {
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@@ -1,73 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.series.recommend_by_genre
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.audio_content.main.v2.series.GetSeriesGenreListResponse
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainNewContentThemeBinding
class AudioContentMainRecommendSeriesGenreAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentMainRecommendSeriesGenreAdapter.ViewHolder>() {
private var items = mutableListOf<GetSeriesGenreListResponse>()
private var selectedGenreId = 0L
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentMainNewContentThemeBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(item: GetSeriesGenreListResponse) {
if (item.id == selectedGenreId) {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
)
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_3bb9f1))
} else {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_777777
)
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_777777))
}
binding.tvTheme.text = item.genre
binding.root.setOnClickListener {
onClickItem(item.id)
selectedGenreId = item.id
notifyDataSetChanged()
}
}
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetSeriesGenreListResponse>) {
this.selectedGenreId = if (items.isNotEmpty()) {
items[0].id
} else {
0
}
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemAudioContentMainNewContentThemeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
}

View File

@@ -2,16 +2,18 @@ package kr.co.vividnext.sodalive.audio_content.modify
import android.Manifest
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.view.setPadding
import coil.load
import coil.transform.RoundedCornersTransformation
import com.github.dhaval2404.imagepicker.ImagePicker
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.jakewharton.rxbinding4.widget.textChanges
@@ -20,6 +22,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.ImagePickerCropper
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentModifyBinding
@@ -33,36 +36,7 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
private val viewModel: AudioContentModifyViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private val imageResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = data?.data
if (fileUri != null) {
binding.ivCover.setPadding(0)
binding.ivCover.background = null
binding.ivCover.load(fileUri) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
viewModel.coverImageUri = fileUri
} else {
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
}
}
private lateinit var cropper: ImagePickerCropper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -82,24 +56,53 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
override fun onDestroy() {
cropper.cleanup()
super.onDestroy()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
cropper = ImagePickerCropper(
caller = this,
context = this,
excludeGif = true,
isEnabledFreeStyleCrop = true,
config = ImagePickerCropper.Config(
aspectX = 1f, aspectY = 1f,
compressFormat = Bitmap.CompressFormat.JPEG,
compressQuality = 90
),
onSuccess = { file, uri ->
binding.ivCover.setPadding(0)
binding.ivCover.background = null
Glide.with(this)
.load(uri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
13.3f.dpToPx().toInt()
)
)
)
.into(binding.ivCover)
viewModel.coverImageFile = file
},
onError = { e ->
Toast.makeText(this, "${e.message}", Toast.LENGTH_SHORT).show()
}
)
binding.toolbar.tvBack.text = "콘텐츠 수정"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.ivPhotoPicker.setOnClickListener {
ImagePicker.with(this)
.crop()
.galleryOnly()
.galleryMimeTypes( // Exclude gif images
mimeTypes = arrayOf(
"image/png",
"image/jpg",
"image/jpeg"
)
)
.createIntent { imageResult.launch(it) }
}
binding.ivPhotoPicker.setOnClickListener { cropper.launch() }
binding.llAvailablePoint.setOnClickListener { viewModel.setAvailablePoint(true) }
binding.llNotAvailablePoint.setOnClickListener { viewModel.setAvailablePoint(false) }
binding.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) }
binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) }
@@ -152,6 +155,15 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
}
)
compositeDisposable.add(
binding.etTag.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
viewModel.tags = it.toString()
}
)
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
@@ -164,6 +176,14 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
}
}
viewModel.isAvailablePointLiveData.observe(this) {
if (it) {
checkAvailablePoint()
} else {
checkNotAvailablePoint()
}
}
viewModel.isAvailableCommentLiveData.observe(this) {
if (it) {
binding.ivCommentYes.visibility = View.VISIBLE
@@ -219,8 +239,8 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
viewModel.setAdult(true)
}
viewModel.isAdultLiveData.observe(this) {
if (it) {
viewModel.isAdultLiveData.observe(this) { isAdult ->
if (isAdult) {
binding.ivAgeAll.visibility = View.GONE
binding.llAgeAll.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
@@ -284,5 +304,53 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
viewModel.detailLiveData.observe(this) {
binding.etDetail.setText(it)
}
viewModel.tagsLiveData.observe(this) {
binding.etTag.setText(it)
}
}
private fun checkAvailablePoint() {
binding.ivAvailablePoint.visibility = View.VISIBLE
binding.tvAvailablePoint.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llAvailablePoint.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivNotAvailablePoint.visibility = View.GONE
binding.tvNotAvailablePoint.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.llNotAvailablePoint.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
}
private fun checkNotAvailablePoint() {
binding.ivNotAvailablePoint.visibility = View.VISIBLE
binding.tvNotAvailablePoint.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llNotAvailablePoint.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivAvailablePoint.visibility = View.GONE
binding.tvAvailablePoint.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.llAvailablePoint.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
}
}

View File

@@ -46,6 +46,10 @@ class AudioContentModifyViewModel(
val detailLiveData: LiveData<String>
get() = _detailLiveData
private val _tagsLiveData = MutableLiveData("")
val tagsLiveData: LiveData<String>
get() = _tagsLiveData
private val _coverImageLiveData = MutableLiveData("")
val coverImageLiveData: LiveData<String>
get() = _coverImageLiveData
@@ -54,12 +58,18 @@ class AudioContentModifyViewModel(
val isAdultShowUiLiveData: LiveData<Boolean>
get() = _isAdultShowUiLiveData
private val _isAvailablePointLiveData = MutableLiveData(false)
val isAvailablePointLiveData: LiveData<Boolean>
get() = _isAvailablePointLiveData
lateinit var getRealPathFromURI: (Uri) -> String?
var contentId: Long = 0
var title: String? = null
var detail: String? = null
var coverImageUri: Uri? = null
var tags: String? = null
var coverImageFile: File? = null
var isPointAvailable: Boolean? = null
fun setAdult(isAdult: Boolean) {
_isAdultLiveData.postValue(isAdult)
@@ -69,6 +79,11 @@ class AudioContentModifyViewModel(
_isAvailableCommentLiveData.postValue(isAvailableComment)
}
fun setAvailablePoint(isAvailablePoint: Boolean) {
isPointAvailable = isAvailablePoint
_isAvailablePointLiveData.value = isAvailablePoint
}
fun getAudioContentDetail(audioContentId: Long, onFailure: (() -> Unit)? = null) {
this.contentId = audioContentId
_isLoading.value = true
@@ -85,10 +100,12 @@ class AudioContentModifyViewModel(
if (it.success && it.data != null) {
_titleLiveData.value = it.data.title
_detailLiveData.value = it.data.detail
_tagsLiveData.value = it.data.tag
_coverImageLiveData.value = it.data.coverImageUrl
_isAvailableCommentLiveData.value = it.data.isCommentAvailable
_isAdultLiveData.value = it.data.isAdult
_isAdultShowUiLiveData.value = !it.data.isAdult
_isAvailablePointLiveData.value = it.data.isAvailableUsePoint
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
@@ -125,14 +142,20 @@ class AudioContentModifyViewModel(
contentId = contentId,
title = title,
detail = detail,
tags = if (tags != _tagsLiveData.value!!) {
tags
} else {
null
},
isAdult = _isAdultLiveData.value!!,
isPointAvailable = isPointAvailable,
isCommentAvailable = _isAvailableCommentLiveData.value!!
)
val requestJson = Gson().toJson(request)
val coverImage = if (coverImageUri != null) {
val file = File(getRealPathFromURI(coverImageUri!!))
val coverImage = if (coverImageFile != null) {
val file = coverImageFile!!
MultipartBody.Part.createFormData(
"coverImage",
file.name,

View File

@@ -8,6 +8,8 @@ data class ModifyAudioContentRequest(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String?,
@SerializedName("detail") val detail: String?,
@SerializedName("tags") val tags: String?,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isPointAvailable") val isPointAvailable: Boolean?,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean
)

View File

@@ -29,6 +29,7 @@ class AudioContentOrderConfirmDialog(
duration: String,
orderType: OrderType,
price: Int,
isAvailableUsePoint: Boolean,
confirmButtonClick: () -> Unit,
) {
@@ -62,12 +63,52 @@ class AudioContentOrderConfirmDialog(
dialogView.tvDuration.text = duration
if (SharedPreferenceManager.userId == 17958L) {
dialogView.ivCan.visibility = View.GONE
dialogView.tvPrice.text = "${(price * 110).moneyFormat()}"
val maxUsablePoint = if (orderType == OrderType.RENTAL && isAvailableUsePoint) {
price * 10
} else {
dialogView.ivCan.visibility = View.VISIBLE
dialogView.tvPrice.text = price.moneyFormat()
0
}
val totalAvailablePoint = if (orderType == OrderType.RENTAL && isAvailableUsePoint) {
SharedPreferenceManager.point
} else {
0
}
val usablePoint = (minOf(totalAvailablePoint, maxUsablePoint) / 10) * 10
if (SharedPreferenceManager.userId == 17958L) {
dialogView.ivPoint.visibility = View.GONE
dialogView.tvPoint.visibility = View.GONE
dialogView.tvPlus.visibility = View.GONE
dialogView.ivCan.visibility = View.GONE
dialogView.tvCan.text = "${(price * 110).moneyFormat()}"
} else {
if (usablePoint > 0) {
dialogView.ivPoint.visibility = View.VISIBLE
dialogView.tvPoint.visibility = View.VISIBLE
dialogView.tvPoint.text = usablePoint.moneyFormat()
} else {
dialogView.ivPoint.visibility = View.GONE
dialogView.tvPoint.visibility = View.GONE
}
val remainingCan = ((price * 10) - usablePoint) / 10
dialogView.tvPlus.visibility = if (usablePoint > 0 && remainingCan > 0) {
View.VISIBLE
} else {
View.GONE
}
if (remainingCan > 0) {
dialogView.ivCan.visibility = View.VISIBLE
dialogView.tvCan.visibility = View.VISIBLE
dialogView.tvCan.text = remainingCan.moneyFormat()
} else {
dialogView.ivCan.visibility = View.GONE
dialogView.tvCan.visibility = View.GONE
}
}
if (SharedPreferenceManager.userId == 17958L) {
@@ -78,9 +119,9 @@ class AudioContentOrderConfirmDialog(
}
} else {
dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
"콘텐츠를 대여하시겠습니까?\n아래 이 차감됩니다."
"콘텐츠를 대여하시겠습니까?\n아래 금액이 차감됩니다."
} else {
"콘텐츠를 소장하시겠습니까?\n아래 이 차감됩니다."
"콘텐츠를 소장하시겠습니까?\n아래 금액이 차감됩니다."
}
}

View File

@@ -40,6 +40,9 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.Utils
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentPlayerBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.mypage.recent.RecentContentViewModel
import kr.co.vividnext.sodalive.mypage.recent.db.RecentContent
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
@UnstableApi
@@ -53,6 +56,7 @@ class AudioContentPlayerFragment(
private lateinit var binding: FragmentAudioContentPlayerBinding
private val viewModel: AudioContentPlayerViewModel by viewModel()
private val recentContentViewModel: RecentContentViewModel by inject()
private var mediaController: MediaController? = null
private val handler = Handler(Looper.getMainLooper())
@@ -451,7 +455,19 @@ class AudioContentPlayerFragment(
transformations(RoundedCornersTransformation(8f.dpToPx()))
}
adapter.updateCurrentPlayingId(it.extras?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID))
val contentId = it.extras?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID)
adapter.updateCurrentPlayingId(contentId)
// Save to recent content
contentId?.let { id ->
val recentContent = RecentContent(
contentId = id,
coverImageUrl = it.artworkUri.toString(),
title = it.title.toString(),
creatorNickname = it.artist.toString()
)
recentContentViewModel.insertRecentContent(recentContent)
}
}
}

View File

@@ -13,10 +13,12 @@ import retrofit2.http.Query
interface SeriesApi {
@GET("/audio-content/series")
fun getSeriesList(
@Query("creatorId") creatorId: Long,
@Query("creatorId") creatorId: Long?,
@Query("sortType") sortType: SeriesListAllViewModel.SeriesSortType,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("isOriginal") isOriginal: Boolean?,
@Query("isCompleted") isCompleted: Boolean?,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
@@ -38,11 +40,4 @@ interface SeriesApi {
@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

@@ -33,7 +33,6 @@ class SeriesListAdapter(
binding.clCover.layoutParams = lp
binding.ivCover.load(item.coverImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5f.dpToPx()))
}

View File

@@ -9,9 +9,11 @@ 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.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivitySeriesListAllBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.home.HomeSeriesAdapter
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
@@ -22,55 +24,65 @@ class SeriesListAllActivity : BaseActivity<ActivitySeriesListAllBinding>(
private val viewModel: SeriesListAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var seriesAdapter: SeriesListAdapter
private lateinit var seriesAdapter: HomeSeriesAdapter
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()
}
val passedCreatorId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
val isOriginal = intent.getBooleanExtra(Constants.EXTRA_IS_ORIGINAL, false)
val isCompleted = intent.getBooleanExtra(Constants.EXTRA_IS_COMPLETED, false)
bindData()
viewModel.creatorId = creatorId
viewModel.creatorId = if (passedCreatorId > 0) {
passedCreatorId
} else {
null
}
viewModel.isCompleted = if (isCompleted) {
true
} else {
null
}
viewModel.isOriginal = if (isOriginal) {
true
} else {
null
}
viewModel.getSeriesList()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "시리즈 전체보기"
binding.toolbar.tvBack.text =
if (intent.getBooleanExtra(Constants.EXTRA_IS_COMPLETED, false)) {
"완결 시리즈"
} else if (intent.getBooleanExtra(Constants.EXTRA_IS_ORIGINAL, false)) {
"오직 보이스온에서만"
} else {
"시리즈 전체보기"
}
binding.toolbar.tvBack.setOnClickListener { finish() }
seriesAdapter = SeriesListAdapter(
itemWidth = ((screenWidth - (13.3 * 3)) / 3).roundToInt(),
seriesAdapter = HomeSeriesAdapter(
itemWidth = ((screenWidth - 24f.dpToPx() * 2 - 16f.dpToPx()) / 2f).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 spanCount = 2
val spacingPx = 16f.dpToPx().toInt()
val recyclerView = binding.rvSeriesAll
recyclerView.layoutManager = GridLayoutManager(this, spanCount)
recyclerView.addItemDecoration(
DifferentSpacingItemDecoration(
spanCount = spanCount,
horizontalSpacing = horizontalSpacing,
verticalSpacing = verticalSpacing,
includeEdge = true
)
GridSpacingItemDecoration(spanCount, spacingPx, true)
)
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {

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