Compare commits

...

53 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
140 changed files with 3657 additions and 10608 deletions

1
.gitignore vendored
View File

@@ -144,6 +144,7 @@ output.json
hs_err_pid*
### Kotlin ###
.kotlin/
# Compiled class file
# Log file

View File

@@ -63,8 +63,8 @@ android {
applicationId "kr.co.vividnext.sodalive"
minSdk 23
targetSdk 35
versionCode 198
versionName "1.43.0"
versionCode 204
versionName "1.44.0"
}
buildTypes {
@@ -232,6 +232,8 @@ dependencies {
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'

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

@@ -86,13 +86,15 @@
<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"/>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- PayVerse 리다이렉트에 등록할 커스텀 스킴/호스트 -->
<data android:scheme="${URISCHEME}"
<data
android:host="payverse"
android:path="/result"/>
android:path="/result"
android:scheme="${URISCHEME}" />
</intent-filter>
</activity>
<activity
@@ -106,6 +108,7 @@
</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" />
@@ -161,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" />
@@ -177,14 +179,6 @@
<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" />
@@ -204,6 +198,7 @@
</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"

View File

@@ -6,8 +6,6 @@ import io.agora.rtc2.Constants
import io.agora.rtc2.IRtcEngineEventHandler
import io.agora.rtc2.RtcEngine
import io.agora.rtm.ErrorInfo
import io.agora.rtm.GetOnlineUsersOptions
import io.agora.rtm.GetOnlineUsersResult
import io.agora.rtm.PublishOptions
import io.agora.rtm.ResultCallback
import io.agora.rtm.RtmClient
@@ -68,6 +66,7 @@ class Agora(
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)
}

View File

@@ -7,7 +7,6 @@ import kr.co.vividnext.sodalive.audio_content.all.by_theme.GetContentByThemeResp
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
@@ -16,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
@@ -46,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,
@@ -69,6 +73,15 @@ interface AudioContentApi {
@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,
@@ -190,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,
@@ -257,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,
@@ -52,7 +34,7 @@ class AudioContentRepository(
fun getAudioContentReplayLiveList(token: String) = api.getAudioContentReplayLiveList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
authHeader = token
)
@@ -127,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,
@@ -144,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
@@ -152,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
)
@@ -184,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
@@ -218,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

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

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

@@ -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,42 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.view.View
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.ivPoint.visibility = if (item.isPointAvailable) {
View.VISIBLE
} else {
View.GONE
}
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,

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

View File

@@ -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,112 +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
binding.ivPoint.visibility = if (item.isPointAvailable) {
View.VISIBLE
} else {
View.GONE
}
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,126 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.alarm
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.event.EventItem
class AudioContentMainTabAlarmViewModel(
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 _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 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.getContentMainAlarm(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
val themeList = listOf("전체").union(data.alarmThemeList).toList()
_themeListLiveData.value = themeList
_newContentListLiveData.value = data.newAlarmContentList
_eventLiveData.value = data.eventBannerList.eventList
_curationListLiveData.value = data.curationList
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun getContentMainAlarm(selectedTheme: String) {
_isLoading.value = true
compositeDisposable.add(
repository.getContentMainAlarmAll(
theme = if (selectedTheme == "전체") {
""
} else {
selectedTheme
},
page = 1,
size = 10,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_newContentListLiveData.value = it.data.items
} 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,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,89 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.series.origianl_audio_drama
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
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.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.databinding.ItemSeriesOriginalAudioDramaBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainTabSeriesOriginalAudioDramaAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentMainTabSeriesOriginalAudioDramaAdapter.ViewHolder>() {
val items = mutableListOf<GetSeriesListResponse.SeriesListItem>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemSeriesOriginalAudioDramaBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(item: GetSeriesListResponse.SeriesListItem) {
Glide
.with(context)
.load(item.coverImage)
.apply(
RequestOptions().transform(
CenterCrop(),
RoundedCorners(5f.dpToPx().toInt())
)
)
.into(binding.ivCover)
binding.tvTitle.text = item.title
binding.tvSeriesContentCount.text = "${item.numberOfContent}"
binding.tvNew.visibility = if (item.isNew) {
View.VISIBLE
} else {
View.GONE
}
binding.tvPopular.visibility = if (item.isPopular) {
View.VISIBLE
} else {
View.GONE
}
if (item.isComplete) {
binding.tvNew.visibility = View.GONE
binding.tvComplete.visibility = View.VISIBLE
} else {
binding.tvComplete.visibility = View.GONE
}
binding.root.setOnClickListener { onClickItem(item.seriesId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemSeriesOriginalAudioDramaBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.count()
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetSeriesListResponse.SeriesListItem>) {
this.items.addAll(items)
notifyDataSetChanged()
}
fun isVisibleRecyclerView(): Boolean {
return items.isNotEmpty()
}
}

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

@@ -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() {

View File

@@ -28,10 +28,12 @@ class SeriesListAllViewModel(private val repository: SeriesRepository) : BaseVie
val seriesListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _seriesListLiveData
var creatorId = 0L
var creatorId: Long? = null
var isCompleted: Boolean? = null
var isOriginal: Boolean? = null
var isLast = false
var page = 1
private val size = 10
private val size = 20
fun getSeriesList() {
if (!_isLoading.value!! && !isLast) {
@@ -40,6 +42,8 @@ class SeriesListAllViewModel(private val repository: SeriesRepository) : BaseVie
compositeDisposable.add(
repository.getSeriesList(
creatorId = creatorId,
isOriginal = isOriginal,
isCompleted = isCompleted,
sortType = SeriesSortType.NEWEST,
page = page,
size = size,

View File

@@ -5,7 +5,9 @@ import kr.co.vividnext.sodalive.settings.ContentType
class SeriesRepository(private val api: SeriesApi) {
fun getSeriesList(
creatorId: Long,
creatorId: Long?,
isOriginal: Boolean?,
isCompleted: Boolean?,
sortType: SeriesListAllViewModel.SeriesSortType,
page: Int,
size: Int,
@@ -14,7 +16,9 @@ class SeriesRepository(private val api: SeriesApi) {
creatorId = creatorId,
sortType = sortType,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
isOriginal = isOriginal,
isCompleted = isCompleted,
page = page - 1,
size = size,
authHeader = token
@@ -40,10 +44,4 @@ class SeriesRepository(private val api: SeriesApi) {
sortType = sortType,
authHeader = token
)
fun getRecommendSeriesList(token: String) = api.getRecommendSeriesList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
}

View File

@@ -0,0 +1,66 @@
package kr.co.vividnext.sodalive.audio_content.series.main
import com.google.android.material.tabs.TabLayout
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.series.main.by_genre.SeriesMainByGenreFragment
import kr.co.vividnext.sodalive.audio_content.series.main.day_of_week.SeriesMainDayOfWeekFragment
import kr.co.vividnext.sodalive.audio_content.series.main.home.SeriesMainHomeFragment
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.databinding.ActivitySeriesMainBinding
class SeriesMainActivity : BaseActivity<ActivitySeriesMainBinding>(
ActivitySeriesMainBinding::inflate
) {
private var currentTab = 0
override fun setupView() {
binding.toolbar.tvBack.text = "시리즈 전체보기"
binding.toolbar.tvBack.setOnClickListener { finish() }
setupTabs()
}
private fun setupTabs() {
binding.tabLayout.addTab(binding.tabLayout.newTab().setText(""))
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("요일별"))
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("장르별"))
// 탭 선택 리스너 설정
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
currentTab = tab.position
showTabContent(currentTab)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
// 필요한 경우 구현
}
override fun onTabReselected(tab: TabLayout.Tab) {
// 필요한 경우 구현
}
})
// 초기 탭 선택
showTabContent(currentTab)
}
private fun showTabContent(position: Int) {
val fragmentTransaction = supportFragmentManager.beginTransaction()
// 기존 프래그먼트 제거
supportFragmentManager.fragments.forEach {
fragmentTransaction.remove(it)
}
// 선택된 탭에 따라 프래그먼트 표시
val fragment = when (position) {
1 -> SeriesMainDayOfWeekFragment()
2 -> SeriesMainByGenreFragment()
else -> SeriesMainHomeFragment()
}
fragmentTransaction.add(R.id.fl_container, fragment)
fragmentTransaction.commit()
}
}

View File

@@ -0,0 +1,55 @@
package kr.co.vividnext.sodalive.audio_content.series.main
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.audio_content.series.main.by_genre.GetSeriesGenreListResponse
import kr.co.vividnext.sodalive.audio_content.series.main.home.SeriesHomeResponse
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
import kr.co.vividnext.sodalive.settings.ContentType
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface SeriesMainApi {
@GET("/audio-content/series/main")
fun fetchHome(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<SeriesHomeResponse>>
@GET("/audio-content/series/main/recommend")
fun getRecommendSeriesList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
@GET("/audio-content/series/main/day-of-week")
fun getDayOfWeekSeriesList(
@Query("dayOfWeek") dayOfWeek: SeriesPublishedDaysOfWeek,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
@GET("/audio-content/series/main/genre-list")
fun getGenreList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesGenreListResponse>>>
@GET("/audio-content/series/main/list-by-genre")
fun getSeriesListByGenre(
@Query("genreId") genreId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetSeriesListResponse>>
}

View File

@@ -0,0 +1,55 @@
package kr.co.vividnext.sodalive.audio_content.series.main
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
import kr.co.vividnext.sodalive.settings.ContentType
class SeriesMainRepository(
private val api: SeriesMainApi
) {
fun fetchData(token: String) = api.fetchHome(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getRecommendSeriesList(token: String) = api.getRecommendSeriesList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getDayOfWeekSeriesList(
dayOfWeek: SeriesPublishedDaysOfWeek,
page: Int,
size: Int,
token: String
) = api.getDayOfWeekSeriesList(
dayOfWeek = dayOfWeek,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
authHeader = token
)
fun getGenreList(token: String) = api.getGenreList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getSeriesListByGenre(
genreId: Long,
page: Int,
size: Int,
token: String
) = api.getSeriesListByGenre(
genreId = genreId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
authHeader = token
)
}

View File

@@ -0,0 +1,56 @@
package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemHomeContentThemeBinding
class GenreAdapter(
private val onClickItem: (GetSeriesGenreListResponse) -> Unit
) : RecyclerView.Adapter<GenreAdapter.ViewHolder>() {
private val items = mutableListOf<GetSeriesGenreListResponse>()
private var selectedGenreId: Long? = null
inner class ViewHolder(
private val binding: ItemHomeContentThemeBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(item: GetSeriesGenreListResponse) {
binding.tvTheme.text = item.genre
if (item.id == selectedGenreId) {
binding.tvTheme.setBackgroundResource(R.drawable.bg_round_corner_999_3bb9f1)
} else {
binding.tvTheme.setBackgroundResource(R.drawable.bg_round_corner_999_263238)
}
binding.root.setOnClickListener {
selectedGenreId = item.id
onClickItem(item)
notifyDataSetChanged()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemHomeContentThemeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
fun submitList(list: List<GetSeriesGenreListResponse>, preselectId: Long?) {
items.clear()
items.addAll(list)
selectedGenreId = preselectId
notifyDataSetChanged()
}
}

View File

@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.audio_content.main.v2.series
package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName

View File

@@ -0,0 +1,150 @@
package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAdapter
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.FragmentSeriesMainByGenreBinding
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
class SeriesMainByGenreFragment : BaseFragment<FragmentSeriesMainByGenreBinding>(
FragmentSeriesMainByGenreBinding::inflate
) {
private val viewModel: SeriesMainByGenreViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var seriesAdapter: HomeSeriesAdapter
private lateinit var genreAdapter: GenreAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
observeViewModel()
viewModel.loadGenres()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupGenreView()
setupSeriesView()
}
private fun setupGenreView() {
genreAdapter = GenreAdapter { genre ->
seriesAdapter.clear()
viewModel.setGenre(genre.id)
}
val rvGenre = binding.rvGenre
val layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
rvGenre.layoutManager = layoutManager
rvGenre.adapter = genreAdapter
rvGenre.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
val itemCount = parent.adapter?.itemCount ?: 0
when (position) {
0 -> {
outRect.left = 0
outRect.right = 8f.dpToPx().toInt()
}
itemCount - 1 -> {
outRect.left = 8f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 8f.dpToPx().toInt()
outRect.right = 8f.dpToPx().toInt()
}
}
}
})
}
private fun setupSeriesView() {
seriesAdapter = HomeSeriesAdapter(
itemWidth = ((screenWidth - 24f.dpToPx() * 2 - 16f.dpToPx()) / 2f).roundToInt(),
onClickItem = {
startActivity(
Intent(
requireContext(),
SeriesDetailActivity::class.java
).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
}
)
val spanCount = 2
val spacingPx = 16f.dpToPx().toInt()
val recyclerView = binding.rvSeriesByGenre
recyclerView.layoutManager = GridLayoutManager(requireContext(), spanCount)
recyclerView.addItemDecoration(GridSpacingItemDecoration(spanCount, spacingPx, false))
recyclerView.adapter = seriesAdapter
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
if (!recyclerView.canScrollVertically(1) && lastVisibleItemPosition == itemTotalCount) {
viewModel.getSeriesList()
}
}
})
viewModel.seriesListLiveData.observe(viewLifecycleOwner) {
seriesAdapter.addItems(it)
}
}
private fun observeViewModel() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
}
viewModel.genreListLiveData.observe(viewLifecycleOwner) { genres ->
genreAdapter.submitList(genres, viewModel.selectedGenreId)
}
}
}

View File

@@ -0,0 +1,121 @@
package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
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.main.SeriesMainRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class SeriesMainByGenreViewModel(
private val repository: SeriesMainRepository
) : BaseViewModel() {
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _genreListLiveData = MutableLiveData<List<GetSeriesGenreListResponse>>()
val genreListLiveData: LiveData<List<GetSeriesGenreListResponse>>
get() = _genreListLiveData
private val _seriesListLiveData = MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val seriesListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _seriesListLiveData
private var page = 1
private var isLast = false
private val pageSize = 20
private var _selectedGenreId: Long? = null
val selectedGenreId: Long?
get() = _selectedGenreId
fun loadGenres() {
_isLoading.value = true
compositeDisposable.add(
repository.getGenreList(
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
val genres = it.data
if (genres.isNotEmpty()) {
_selectedGenreId = genres.first().id
page = 1
isLast = false
_genreListLiveData.value = genres
getSeriesList()
} else {
_genreListLiveData.value = emptyList()
}
} else {
_toastLiveData.value = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
},
{
_isLoading.value = false
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
)
)
}
fun setGenre(genreId: Long) {
if (_selectedGenreId != genreId) {
_selectedGenreId = genreId
page = 1
isLast = false
// 시리즈 목록 초기화는 Fragment에서 어댑터를 clear로 처리
getSeriesList()
}
}
fun getSeriesList() {
val genreId = _selectedGenreId ?: return
if (isLast) return
if (_isLoading.value == true) return
_isLoading.value = true
compositeDisposable.add(
repository.getSeriesListByGenre(
genreId = genreId,
page = page,
size = pageSize,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
val data = it.data
if (it.success && data != null) {
page += 1
val items = data.items
if (items.isNotEmpty()) {
_seriesListLiveData.value = items
} else {
isLast = true
}
} else {
_toastLiveData.value = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
},
{
_isLoading.value = false
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
)
)
}
}

View File

@@ -0,0 +1,171 @@
package kr.co.vividnext.sodalive.audio_content.series.main.day_of_week
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAdapter
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.FragmentSeriesMainDayOfWeekBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.home.DayOfWeekAdapter
import kr.co.vividnext.sodalive.home.HomeSeriesAdapter
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
import org.koin.android.ext.android.inject
import java.util.Calendar
import kotlin.math.roundToInt
class SeriesMainDayOfWeekFragment : BaseFragment<FragmentSeriesMainDayOfWeekBinding>(
FragmentSeriesMainDayOfWeekBinding::inflate
) {
private val viewModel: SeriesMainDayOfWeekViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: HomeSeriesAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
observeViewModel()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupDayOfWeekDay()
setupSeriesView()
val dayOfWeeks = listOf(
SeriesPublishedDaysOfWeek.RANDOM,
SeriesPublishedDaysOfWeek.SUN,
SeriesPublishedDaysOfWeek.MON,
SeriesPublishedDaysOfWeek.TUE,
SeriesPublishedDaysOfWeek.WED,
SeriesPublishedDaysOfWeek.THU,
SeriesPublishedDaysOfWeek.FRI,
SeriesPublishedDaysOfWeek.SAT
)
val calendar = Calendar.getInstance()
val dayIndex = calendar.get(Calendar.DAY_OF_WEEK)
viewModel.dayOfWeek = dayOfWeeks[dayIndex]
}
private fun setupDayOfWeekDay() {
val dayOfWeekAdapter = DayOfWeekAdapter(screenWidth = screenWidth) {
adapter.clear()
viewModel.dayOfWeek = it
}
val rvDayOfWeek = binding.rvSeriesDayOfWeekDay
val layoutManager = object : LinearLayoutManager(
context,
HORIZONTAL,
false
) {
override fun canScrollVertically() = false
override fun canScrollHorizontally() = false
}
rvDayOfWeek.layoutManager = layoutManager
rvDayOfWeek.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 = 2.5f.dpToPx().toInt()
}
dayOfWeekAdapter.itemCount - 1 -> {
outRect.left = 2.5f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 2.5f.dpToPx().toInt()
outRect.right = 2.5f.dpToPx().toInt()
}
}
}
})
rvDayOfWeek.adapter = dayOfWeekAdapter
}
private fun setupSeriesView() {
adapter = HomeSeriesAdapter(
itemWidth = ((screenWidth - 24f.dpToPx() * 2 - 16f.dpToPx()) / 2f).roundToInt(),
onClickItem = {
startActivity(
Intent(
requireContext(),
SeriesDetailActivity::class.java
).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
}
)
val spanCount = 2
val spacingPx = 16f.dpToPx().toInt()
val recyclerView = binding.rvSeriesDayOfWeek
recyclerView.layoutManager = GridLayoutManager(requireContext(), spanCount)
recyclerView.addItemDecoration(
GridSpacingItemDecoration(spanCount, spacingPx, false)
)
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getSeriesList()
}
}
})
recyclerView.adapter = adapter
viewModel.seriesListLiveData.observe(viewLifecycleOwner) {
adapter.addItems(it)
}
}
private fun observeViewModel() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
}
}
}

View File

@@ -0,0 +1,85 @@
package kr.co.vividnext.sodalive.audio_content.series.main.day_of_week
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.main.SeriesMainRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
class SeriesMainDayOfWeekViewModel(
private val repository: SeriesMainRepository
) : BaseViewModel() {
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _seriesListLiveData = MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val seriesListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _seriesListLiveData
private var page = 1
private var isLast = false
private val pageSize = 20
var dayOfWeek = SeriesPublishedDaysOfWeek.RANDOM
set(newValue) {
if (field != newValue) {
page = 1
isLast = false
field = newValue
getSeriesList()
}
}
fun getSeriesList() {
if (isLast) return
_isLoading.value = true
compositeDisposable.add(
repository.getDayOfWeekSeriesList(
dayOfWeek = dayOfWeek,
page = page,
size = pageSize,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
val data = it.data
if (it.success && data != null) {
page += 1
if (data.isNotEmpty()) {
_seriesListLiveData.value = data
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
)
)
}
}

View File

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

View File

@@ -0,0 +1,23 @@
package kr.co.vividnext.sodalive.audio_content.series.main.home
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
@Keep
data class SeriesHomeResponse(
@SerializedName("banners")
val banners: List<SeriesBannerResponse>,
@SerializedName("completedSeriesList")
val completedSeriesList: List<GetSeriesListResponse.SeriesListItem>,
@SerializedName("recommendSeriesList")
val recommendSeriesList: List<GetSeriesListResponse.SeriesListItem>
)
@Keep
data class SeriesBannerResponse(
@SerializedName("seriesId") val seriesId: Long,
@SerializedName("imagePath") val imagePath: String
)

View File

@@ -0,0 +1,245 @@
package kr.co.vividnext.sodalive.audio_content.series.main.home
import android.content.Intent
import android.graphics.Rect
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.series.SeriesListAllActivity
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.FragmentSeriesMainHomeBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.home.HomeSeriesAdapter
import kr.co.vividnext.sodalive.main.MainActivity
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
class SeriesMainHomeFragment : BaseFragment<FragmentSeriesMainHomeBinding>(
FragmentSeriesMainHomeBinding::inflate
) {
private val viewModel: SeriesMainHomeViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var bannerAdapter: SeriesBannerAdapter
private lateinit var completedAdapter: HomeSeriesAdapter
private lateinit var recommendAdapter: HomeSeriesAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
observeViewModel()
viewModel.fetchData()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupBanner()
setupCompletedSeriesView()
setupRecommendSeriesView()
}
private fun setupBanner() {
val layoutParams = binding
.bannerSlider
.layoutParams as LinearLayout.LayoutParams
val pagerWidth = screenWidth
val pagerHeight = pagerWidth * 198 / 352
layoutParams.width = pagerWidth
layoutParams.height = pagerHeight
bannerAdapter = SeriesBannerAdapter(
requireContext(),
pagerWidth,
pagerHeight
) {
startActivity(
Intent(
requireContext(),
SeriesDetailActivity::class.java
).apply {
putExtra(Constants.EXTRA_SERIES_ID, it.seriesId)
}
)
}
binding
.bannerSlider
.layoutParams = layoutParams
binding.bannerSlider.apply {
adapter = bannerAdapter as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(1000)
setInterval(4 * 1000)
}.create()
binding
.bannerSlider
.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(10f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(10f.dpToPx().toInt())
viewModel.bannerListLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llBanner.visibility = View.VISIBLE
binding.bannerSlider.refreshData(it)
} else {
binding.llBanner.visibility = View.GONE
}
}
}
@OptIn(UnstableApi::class)
private fun setupCompletedSeriesView() {
completedAdapter = HomeSeriesAdapter {
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
}
val recyclerView = binding.rvCompletedSeries
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 = 8f.dpToPx().toInt()
}
completedAdapter.itemCount - 1 -> {
outRect.left = 8f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 8f.dpToPx().toInt()
outRect.right = 8f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = completedAdapter
binding.tvCompletedSeriesAll.setOnClickListener {
startActivity(
Intent(
requireContext(),
SeriesListAllActivity::class.java
).apply {
putExtra(Constants.EXTRA_IS_COMPLETED, true)
}
)
}
viewModel.completedSeriesLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llCompletedSeries.visibility = View.VISIBLE
completedAdapter.addItems(it)
} else {
binding.llCompletedSeries.visibility = View.GONE
}
}
}
private fun setupRecommendSeriesView() {
recommendAdapter = HomeSeriesAdapter(
itemWidth = ((screenWidth - 24f.dpToPx() * 2 - 16f.dpToPx()) / 2f).roundToInt(),
onClickItem = {
startActivity(
Intent(
requireContext(),
SeriesDetailActivity::class.java
).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
}
)
val spanCount = 2
val spacingPx = 16f.dpToPx().toInt()
val recyclerView = binding.rvRecommendSeries
recyclerView.layoutManager = GridLayoutManager(requireContext(), spanCount)
recyclerView.addItemDecoration(
GridSpacingItemDecoration(spanCount, spacingPx, false)
)
recyclerView.adapter = recommendAdapter
binding.ivRecommendRefresh.setOnClickListener {
viewModel.getRecommendSeriesList()
}
viewModel.recommendSeriesLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llRecommendSeries.visibility = View.VISIBLE
recommendAdapter.clear()
recommendAdapter.addItems(it)
} else {
binding.llRecommendSeries.visibility = View.GONE
}
}
}
private fun observeViewModel() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
}
}
}

View File

@@ -1,44 +1,56 @@
package kr.co.vividnext.sodalive.audio_content.main.new_content
package kr.co.vividnext.sodalive.audio_content.series.main.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.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentMainNewContentViewModel(
private val repository: AudioContentRepository
class SeriesMainHomeViewModel(
private val repository: SeriesMainRepository
) : 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 val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _themeListLiveData = MutableLiveData<List<String>>()
val themeListLiveData: LiveData<List<String>>
get() = _themeListLiveData
private var _bannerListLiveData = MutableLiveData<List<SeriesBannerResponse>>()
val bannerListLiveData: LiveData<List<SeriesBannerResponse>>
get() = _bannerListLiveData
private var _completedSeriesLiveData =
MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val completedSeriesLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _completedSeriesLiveData
private var _recommendSeriesLiveData =
MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val recommendSeriesLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _recommendSeriesLiveData
fun fetchData() {
_isLoading.value = true
fun getThemeList() {
compositeDisposable.add(
repository.getNewContentThemeList(token = "Bearer ${SharedPreferenceManager.token}")
repository.fetchData(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)
_isLoading.value = false
val data = it.data
if (it.success && data != null) {
_bannerListLiveData.value = data.banners
_completedSeriesLiveData.value = data.completedSeriesList
_recommendSeriesLiveData.value = data.recommendSeriesList
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
@@ -48,8 +60,6 @@ class AudioContentMainNewContentViewModel(
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
@@ -60,39 +70,30 @@ class AudioContentMainNewContentViewModel(
)
}
fun getNewContentOfTheme(theme: String) {
fun getRecommendSeriesList() {
_isLoading.value = true
compositeDisposable.add(
repository.getNewContentOfTheme(
theme = if (theme == "전체") {
""
} else {
theme
},
token = "Bearer ${SharedPreferenceManager.token}"
)
repository.getRecommendSeriesList(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
_newContentListLiveData.value = it.data!!
_recommendSeriesLiveData.value = it.data
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
_toastLiveData.value = it.message
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
_isLoading.value = false
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
)
)

View File

@@ -17,6 +17,7 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.viewbinding.ViewBinding
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlin.math.max
abstract class BaseActivity<T : ViewBinding>(
private val inflate: (LayoutInflater) -> T
@@ -66,10 +67,15 @@ abstract class BaseActivity<T : ViewBinding>(
setupView()
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
// 루트는 좌/우/하만 처리(상단은 Toolbar에 위임)
v.setPadding(bars.left, bars.top, bars.right, bars.bottom)
// 루트는 좌/우/하만 처리(상단은 Toolbar에 위임). IME가 등장하면 하단 패딩을 IME 높이까지 확장
val left = max(systemBars.left, ime.left)
val top = systemBars.top
val right = max(systemBars.right, ime.right)
val bottom = max(systemBars.bottom, ime.bottom)
v.setPadding(left, top, right, bottom)
insets
}

View File

@@ -7,6 +7,7 @@ import com.google.android.material.tabs.TabLayout
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.chat.character.CharacterTabFragment
import kr.co.vividnext.sodalive.chat.original.OriginalTabFragment
import kr.co.vividnext.sodalive.chat.talk.TalkTabFragment
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentChatBinding
@@ -52,6 +53,7 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
private fun setupTabs() {
// 탭 추가
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("캐릭터"))
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("작품별"))
binding.tabLayout.addTab(binding.tabLayout.newTab().setText(""))
// 탭 선택 리스너 설정
@@ -85,8 +87,8 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
// 선택된 탭에 따라 프래그먼트 표시
val fragment = when (position) {
0 -> CharacterTabFragment()
1 -> TalkTabFragment()
1 -> OriginalTabFragment()
2 -> TalkTabFragment()
else -> CharacterTabFragment()
}

View File

@@ -11,5 +11,6 @@ data class Character(
@SerializedName("characterId") val characterId: Long,
@SerializedName("name") val name: String,
@SerializedName("description") val description: String,
@SerializedName("imageUrl") val imageUrl: String
@SerializedName("imageUrl") val imageUrl: String,
@SerializedName("isNew") val isNew: Boolean
) : Parcelable

View File

@@ -49,6 +49,12 @@ class CharacterAdapter(
binding.tvRanking.visibility = View.GONE
}
binding.tvNew.visibility = if (character.isNew) {
View.VISIBLE
} else {
View.GONE
}
binding.ivCharacter.load(character.imageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo_service_center)

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