From 1329ae5e5d95cdc8607bcf23fd9b5fda97296db5 Mon Sep 17 00:00:00 2001 From: klaus Date: Sat, 5 Aug 2023 01:25:09 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 8 +- app/objectbox-models/default.json | 67 ++ app/src/main/AndroidManifest.xml | 38 +- .../AddAllPlaybackTrackingRequest.kt | 15 + .../audio_content/AudioContentActivity.kt | 212 +++++ .../audio_content/AudioContentAdapter.kt | 73 ++ .../sodalive/audio_content/AudioContentApi.kt | 140 ++++ .../audio_content/AudioContentPlayService.kt | 556 ++++++++++++ .../audio_content/AudioContentRepository.kt | 138 +++ .../audio_content/AudioContentViewModel.kt | 102 +++ .../audio_content/PlaybackTracking.kt | 23 + .../PlaybackTrackingRepository.kt | 29 + .../comment/AudioContentCommentAdapter.kt | 103 +++ .../comment/AudioContentCommentFragment.kt | 72 ++ .../AudioContentCommentListFragment.kt | 182 ++++ .../AudioContentCommentListViewModel.kt | 125 +++ .../AudioContentCommentReplyAdapter.kt | 93 +++ .../AudioContentCommentReplyFragment.kt | 204 +++++ .../AudioContentCommentReplyViewModel.kt | 120 +++ .../comment/AudioContentCommentRepository.kt | 46 + .../GetAudioContentCommentListResponse.kt | 21 + .../RegisterAudioContentCommentRequest.kt | 9 + .../detail/AudioContentDeleteDialog.kt | 67 ++ .../detail/AudioContentDetailActivity.kt | 788 ++++++++++++++++++ .../detail/AudioContentDetailViewModel.kt | 475 +++++++++++ .../detail/AudioContentReportDialog.kt | 60 ++ .../detail/GetAudioContentDetailResponse.kt | 45 + .../detail/OtherContentAdapter.kt | 46 + .../detail/PutAudioContentLikeRequest.kt | 11 + .../donation/AudioContentDonationRequest.kt | 10 + .../main/AudioContentMainBannerAdapter.kt | 41 + .../main/AudioContentMainContentAdapter.kt | 41 + .../main/AudioContentMainCurationAdapter.kt | 94 +++ .../main/AudioContentMainFragment.kt | 470 +++++++++++ .../main/AudioContentMainItemViewHolder.kt | 41 + ...udioContentMainNewContentCreatorAdapter.kt | 52 ++ .../AudioContentMainNewContentThemeAdapter.kt | 68 ++ .../main/AudioContentMainViewModel.kt | 121 +++ .../main/GetAudioContentMainResponse.kt | 55 ++ .../modify/AudioContentModifyActivity.kt | 288 +++++++ .../modify/AudioContentModifyViewModel.kt | 212 +++++ .../modify/ModifyAudioContentRequest.kt | 11 + .../order/AudioContentOrderConfirmDialog.kt | 100 +++ .../order/AudioContentOrderFragment.kt | 44 + .../order/AudioContentOrderListActivity.kt | 110 +++ .../order/AudioContentOrderListAdapter.kt | 61 ++ .../order/AudioContentOrderListViewModel.kt | 77 ++ .../order/GetAudioContentOrderListResponse.kt | 20 + .../audio_content/order/OrderRequest.kt | 9 + .../sodalive/audio_content/order/OrderType.kt | 11 + .../upload/AudioContentUploadActivity.kt | 435 ++++++++++ .../upload/AudioContentUploadViewModel.kt | 221 +++++ .../upload/CreateAudioContentRequest.kt | 13 + .../upload/theme/AudioContentThemeAdapter.kt | 64 ++ .../upload/theme/AudioContentThemeFragment.kt | 102 +++ .../theme/AudioContentThemeViewModel.kt | 47 ++ .../theme/GetAudioContentThemeResponse.kt | 9 + .../co/vividnext/sodalive/common/Constants.kt | 21 +- .../co/vividnext/sodalive/common/ObjectBox.kt | 14 + .../common/SharedPreferenceManager.kt | 12 + .../kr/co/vividnext/sodalive/common/Utils.kt | 12 + .../content/main/ContentMainFragment.kt | 9 - .../java/kr/co/vividnext/sodalive/di/AppDI.kt | 24 + .../fcm/SodaFirebaseMessagingService.kt | 2 +- .../vividnext/sodalive/main/MainActivity.kt | 4 +- .../sodalive/report/ReportRequest.kt | 2 +- .../sodalive/splash/SplashActivity.kt | 2 +- .../kr/co/vividnext/sodalive/user/UserApi.kt | 4 +- .../res/drawable-xhdpi/btn_player_repeat.png | Bin 0 -> 973 bytes .../drawable-xhdpi/btn_player_repeat_done.png | Bin 0 -> 1037 bytes .../btn_audio_content_pause.png | Bin 0 -> 3129 bytes .../btn_audio_content_play.png | Bin 0 -> 3911 bytes .../ic_audio_content_heart_normal.png | Bin 0 -> 758 bytes .../ic_audio_content_heart_pressed.png | Bin 0 -> 606 bytes .../ic_audio_content_share.png | Bin 0 -> 708 bytes .../res/drawable-xxhdpi/ic_circle_x_white.png | Bin 0 -> 1244 bytes .../main/res/drawable-xxhdpi/ic_heart_777.png | Bin 0 -> 830 bytes .../drawable-xxhdpi/ic_message_square_777.png | Bin 0 -> 471 bytes .../res/drawable-xxhdpi/ic_noti_pause.png | Bin 0 -> 442 bytes .../main/res/drawable-xxhdpi/ic_noti_play.png | Bin 0 -> 670 bytes .../main/res/drawable-xxhdpi/ic_noti_stop.png | Bin 0 -> 556 bytes .../ic_notice_exclamation_mark.png | Bin 0 -> 2989 bytes .../main/res/drawable-xxhdpi/ic_time_l.png | Bin 0 -> 910 bytes .../drawable/audio_content_player_seekbar.xml | 22 + app/src/main/res/drawable/bg_black.xml | 5 + .../drawable/bg_round_corner_10_7_2d7390.xml | 8 + .../drawable/bg_round_corner_10_7_4d6aa4.xml | 8 + .../drawable/bg_round_corner_10_7_548f7d.xml | 8 + .../drawable/bg_round_corner_10_7_59548f.xml | 8 + .../drawable/bg_round_corner_10_7_973a3a.xml | 8 + .../drawable/bg_round_corner_10_7_d38c38.xml | 8 + .../drawable/bg_round_corner_10_7_d85e37.xml | 8 + .../drawable/bg_round_corner_13_3_303030.xml | 8 + .../bg_round_corner_26_7_19ffffff.xml | 6 + .../bg_round_corner_26_7_26ffffff.xml | 8 + .../res/drawable/bg_round_corner_2_28312b.xml | 8 + .../drawable/bg_round_corner_2_6_222222.xml | 8 + .../drawable/bg_round_corner_2_6_26310f.xml | 8 + .../drawable/bg_round_corner_2_6_28312b.xml | 8 + .../drawable/bg_round_corner_2_6_30176f.xml | 8 + .../drawable/bg_round_corner_2_6_601d14.xml | 8 + .../drawable/bg_round_corner_2_6_660fd4.xml | 6 + .../drawable/bg_round_corner_2_6_b1ef2c.xml | 6 + .../drawable/bg_round_corner_44_9970ff.xml | 8 + .../drawable/bg_round_corner_5_3_000000.xml | 8 + .../drawable/bg_round_corner_5_3_19ffffff.xml | 8 + .../bg_round_corner_5_3_339970ff_9970ff.xml | 8 + .../bg_round_corner_5_3_e51e0e45_9970ff.xml | 8 + .../bg_round_corner_6_7_1f1734_9970ff.xml | 8 + .../bg_round_corner_6_7_333333_979797.xml | 8 + .../drawable/bg_top_round_corner_8_222222.xml | 10 + .../res/layout/activity_audio_content.xml | 115 +++ .../layout/activity_audio_content_detail.xml | 702 ++++++++++++++++ .../layout/activity_audio_content_modify.xml | 393 +++++++++ .../activity_audio_content_order_list.xml | 17 + .../layout/activity_audio_content_upload.xml | 667 +++++++++++++++ .../layout/dialog_audio_content_comment.xml | 11 + .../layout/dialog_audio_content_delete.xml | 98 +++ .../dialog_audio_content_order_confirm.xml | 194 +++++ .../layout/dialog_audio_content_report.xml | 144 ++++ .../fragment_audio_content_comment_list.xml | 134 +++ .../fragment_audio_content_comment_reply.xml | 125 +++ .../layout/fragment_audio_content_main.xml | 168 ++++ .../layout/fragment_audio_content_order.xml | 122 +++ .../layout/fragment_audio_content_theme.xml | 38 + .../main/res/layout/item_audio_content.xml | 168 ++++ .../res/layout/item_audio_content_comment.xml | 112 +++ .../item_audio_content_comment_reply.xml | 56 ++ .../res/layout/item_audio_content_main.xml | 70 ++ .../item_audio_content_main_curation.xml | 46 + ...audio_content_main_new_content_creator.xml | 33 + ...m_audio_content_main_new_content_theme.xml | 20 + .../layout/item_audio_content_order_list.xml | 188 +++++ .../res/layout/item_audio_content_theme.xml | 44 + .../res/layout/item_audio_other_content.xml | 25 + app/src/main/res/values/colors.xml | 24 + 136 files changed, 10725 insertions(+), 21 deletions(-) create mode 100644 app/objectbox-models/default.json create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/AddAllPlaybackTrackingRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentApi.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentPlayService.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentRepository.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/PlaybackTracking.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/PlaybackTrackingRepository.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentFragment.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentListFragment.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentListViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyFragment.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentRepository.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/GetAudioContentCommentListResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/RegisterAudioContentCommentRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDeleteDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentReportDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/GetAudioContentDetailResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/OtherContentAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/PutAudioContentLikeRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/donation/AudioContentDonationRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainBannerAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainContentAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainCurationAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainFragment.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainItemViewHolder.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainNewContentCreatorAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainNewContentThemeAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/GetAudioContentMainResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/AudioContentModifyActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/AudioContentModifyViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/ModifyAudioContentRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderConfirmDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderFragment.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/GetAudioContentOrderListResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/OrderRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/OrderType.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/AudioContentUploadActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/AudioContentUploadViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/CreateAudioContentRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/AudioContentThemeAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/AudioContentThemeFragment.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/AudioContentThemeViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/GetAudioContentThemeResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/common/ObjectBox.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/common/Utils.kt delete mode 100644 app/src/main/java/kr/co/vividnext/sodalive/content/main/ContentMainFragment.kt create mode 100644 app/src/main/res/drawable-xhdpi/btn_player_repeat.png create mode 100644 app/src/main/res/drawable-xhdpi/btn_player_repeat_done.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_audio_content_pause.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_audio_content_play.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_audio_content_heart_normal.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_audio_content_heart_pressed.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_audio_content_share.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_circle_x_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_heart_777.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_message_square_777.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_noti_pause.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_noti_play.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_noti_stop.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_notice_exclamation_mark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_time_l.png create mode 100644 app/src/main/res/drawable/audio_content_player_seekbar.xml create mode 100644 app/src/main/res/drawable/bg_black.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_10_7_2d7390.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_10_7_4d6aa4.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_10_7_548f7d.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_10_7_59548f.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_10_7_973a3a.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_10_7_d38c38.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_10_7_d85e37.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_13_3_303030.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_26_7_19ffffff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_26_7_26ffffff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_2_28312b.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_2_6_222222.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_2_6_26310f.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_2_6_28312b.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_2_6_30176f.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_2_6_601d14.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_2_6_660fd4.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_2_6_b1ef2c.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_44_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_5_3_000000.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_5_3_19ffffff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_5_3_339970ff_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_5_3_e51e0e45_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_1f1734_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_333333_979797.xml create mode 100644 app/src/main/res/drawable/bg_top_round_corner_8_222222.xml create mode 100644 app/src/main/res/layout/activity_audio_content.xml create mode 100644 app/src/main/res/layout/activity_audio_content_detail.xml create mode 100644 app/src/main/res/layout/activity_audio_content_modify.xml create mode 100644 app/src/main/res/layout/activity_audio_content_order_list.xml create mode 100644 app/src/main/res/layout/activity_audio_content_upload.xml create mode 100644 app/src/main/res/layout/dialog_audio_content_comment.xml create mode 100644 app/src/main/res/layout/dialog_audio_content_delete.xml create mode 100644 app/src/main/res/layout/dialog_audio_content_order_confirm.xml create mode 100644 app/src/main/res/layout/dialog_audio_content_report.xml create mode 100644 app/src/main/res/layout/fragment_audio_content_comment_list.xml create mode 100644 app/src/main/res/layout/fragment_audio_content_comment_reply.xml create mode 100644 app/src/main/res/layout/fragment_audio_content_main.xml create mode 100644 app/src/main/res/layout/fragment_audio_content_order.xml create mode 100644 app/src/main/res/layout/fragment_audio_content_theme.xml create mode 100644 app/src/main/res/layout/item_audio_content.xml create mode 100644 app/src/main/res/layout/item_audio_content_comment.xml create mode 100644 app/src/main/res/layout/item_audio_content_comment_reply.xml create mode 100644 app/src/main/res/layout/item_audio_content_main.xml create mode 100644 app/src/main/res/layout/item_audio_content_main_curation.xml create mode 100644 app/src/main/res/layout/item_audio_content_main_new_content_creator.xml create mode 100644 app/src/main/res/layout/item_audio_content_main_new_content_theme.xml create mode 100644 app/src/main/res/layout/item_audio_content_order_list.xml create mode 100644 app/src/main/res/layout/item_audio_content_theme.xml create mode 100644 app/src/main/res/layout/item_audio_other_content.xml diff --git a/app/build.gradle b/app/build.gradle index 45d4c9e..0aeb36e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -61,8 +61,8 @@ android { buildConfigField 'String', 'BASE_URL', '"https://test-api.sodalive.net"' buildConfigField 'String', 'BOOTPAY_APP_ID', '"6242a7772701800023f68b2e"' - buildConfigField 'String', 'AGORA_APP_ID', '"d28c80855d314a599cd7c15280920699"' - buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"29ef33b7c37e4b80b74af9a6e9b2af5e"' + buildConfigField 'String', 'AGORA_APP_ID', '"b96574e191a9430fa54c605528aa3ef7"' + buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"ae18ade3afcf4086bd4397726eb0654c"' } } compileOptions { @@ -142,4 +142,8 @@ dependencies { // sound visualizer implementation "com.gauravk.audiovisualizer:audiovisualizer:0.9.2" + + // Glide + implementation 'com.github.bumptech.glide:glide:4.12.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' } diff --git a/app/objectbox-models/default.json b/app/objectbox-models/default.json new file mode 100644 index 0000000..8a6b8f1 --- /dev/null +++ b/app/objectbox-models/default.json @@ -0,0 +1,67 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "1:2209417227252155460", + "lastPropertyId": "8:7803281435927194929", + "name": "PlaybackTracking", + "properties": [ + { + "id": "1:3889922602505997244", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:874896374244616380", + "name": "contentId", + "type": 6 + }, + { + "id": "3:305496269372931228", + "name": "totalDuration", + "type": 5 + }, + { + "id": "4:1202262957765031780", + "name": "startPosition", + "type": 5 + }, + { + "id": "5:1595250877919247629", + "name": "isFree", + "type": 1 + }, + { + "id": "6:4066577743967565922", + "name": "isPreview", + "type": 1 + }, + { + "id": "7:7482414752180672089", + "name": "endPosition", + "type": 5 + }, + { + "id": "8:7803281435927194929", + "name": "playDateTime", + "type": 9 + } + ], + "relations": [] + } + ], + "lastEntityId": "1:2209417227252155460", + "lastIndexId": "0:0", + "lastRelationId": "0:0", + "lastSequenceId": "0:0", + "modelVersion": 5, + "modelVersionParserMinimum": 5, + "retiredEntityUids": [], + "retiredIndexUids": [], + "retiredPropertyUids": [], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ad5a1ca..774db06 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,9 +2,31 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + +) + +data class PlaybackTrackingData( + @SerializedName("contentId") val contentId: Long, + @SerializedName("playDateTime") val playDateTime: String, + @SerializedName("isPreview") val isPreview: Boolean, +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentActivity.kt new file mode 100644 index 0000000..178cb01 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentActivity.kt @@ -0,0 +1,212 @@ +package kr.co.vividnext.sodalive.audio_content + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.graphics.Rect +import android.os.Bundle +import android.view.View +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity +import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.databinding.ActivityAudioContentBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject + +class AudioContentActivity : BaseActivity( + ActivityAudioContentBinding::inflate +) { + + private val viewModel: AudioContentViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + private lateinit var audioContentAdapter: AudioContentAdapter + + private var userId: Long = 0 + private lateinit var activityResultLauncher: ActivityResultLauncher + + override fun onCreate(savedInstanceState: Bundle?) { + userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0) + activityResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == Activity.RESULT_OK) { + viewModel.page = 1 + viewModel.getAudioContentList(userId = userId) { finish() } + } + } + super.onCreate(savedInstanceState) + + if (userId <= 0) { + Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show() + finish() + } + + bindData() + viewModel.getAudioContentList(userId = userId) { finish() } + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + binding.toolbar.tvBack.text = "콘텐츠 전체보기" + binding.toolbar.tvBack.setOnClickListener { finish() } + + audioContentAdapter = AudioContentAdapter { + val intent = Intent(applicationContext, AudioContentDetailActivity::class.java) + .apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) + } + activityResultLauncher.launch(intent) + } + + binding.rvAudioContent.layoutManager = LinearLayoutManager( + applicationContext, + LinearLayoutManager.VERTICAL, + false + ) + + binding.rvAudioContent.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + when (parent.getChildAdapterPosition(view)) { + audioContentAdapter.itemCount - 1 -> { + outRect.bottom = 0 + } + + else -> { + outRect.bottom = 13.3f.dpToPx().toInt() + } + } + } + }) + + binding.rvAudioContent.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!! + .findLastCompletelyVisibleItemPosition() + val itemTotalCount = recyclerView.adapter!!.itemCount - 1 + + // 스크롤이 끝에 도달했는지 확인 + if (!recyclerView.canScrollVertically(1) && + lastVisibleItemPosition == itemTotalCount + ) { + viewModel.getAudioContentList(userId = userId) { } + } + } + }) + + binding.rvAudioContent.adapter = audioContentAdapter + + binding.tvSortNewest.setOnClickListener { + viewModel.changeSort(AudioContentViewModel.Sort.NEWEST) + } + + binding.tvSortPriceLow.setOnClickListener { + viewModel.changeSort(AudioContentViewModel.Sort.PRICE_LOW) + } + + binding.tvSortPriceHigh.setOnClickListener { + viewModel.changeSort(AudioContentViewModel.Sort.PRICE_HIGH) + } + + if (userId == SharedPreferenceManager.userId) { + binding.tvNewContent.visibility = View.VISIBLE + binding.tvNewContent.setOnClickListener { + startActivity( + Intent( + applicationContext, + AudioContentUploadActivity::class.java + ) + ) + } + } else { + binding.tvNewContent.visibility = View.GONE + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun bindData() { + viewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + viewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth, "") + } else { + loadingDialog.dismiss() + } + } + + viewModel.audioContentListLiveData.observe(this) { + if (viewModel.page - 1 == 1) { + audioContentAdapter.items.clear() + binding.rvAudioContent.scrollToPosition(0) + } + + binding.tvTotalCount.text = "${it.totalCount}" + + audioContentAdapter.items.addAll(it.items) + audioContentAdapter.notifyDataSetChanged() + } + + viewModel.sort.observe(this) { + deselectSort() + selectSort( + when (it) { + AudioContentViewModel.Sort.PRICE_HIGH -> { + binding.tvSortPriceHigh + } + + AudioContentViewModel.Sort.PRICE_LOW -> { + binding.tvSortPriceLow + } + + else -> { + binding.tvSortNewest + } + } + ) + viewModel.getAudioContentList(userId = userId) { finish() } + } + } + + private fun deselectSort() { + val color = ContextCompat.getColor( + applicationContext, + R.color.color_88e2e2e2 + ) + + binding.tvSortNewest.setTextColor(color) + binding.tvSortPriceLow.setTextColor(color) + binding.tvSortPriceHigh.setTextColor(color) + } + + private fun selectSort(view: TextView) { + view.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_e2e2e2 + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentAdapter.kt new file mode 100644 index 0000000..e52ad09 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentAdapter.kt @@ -0,0 +1,73 @@ +package kr.co.vividnext.sodalive.audio_content + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemAudioContentBinding +import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListItem +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.extensions.moneyFormat + +class AudioContentAdapter( + private val onClickItem: (Long) -> Unit +) : RecyclerView.Adapter() { + + val items = mutableListOf() + + inner class ViewHolder( + private val binding: ItemAudioContentBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: GetAudioContentListItem) { + binding.ivCover.load(item.coverImageUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(5.3f.dpToPx())) + } + + binding.tvTitle.text = item.title + binding.tvTheme.text = item.themeStr + binding.tvDuration.text = item.duration + binding.tvLikeCount.text = item.likeCount.moneyFormat() + binding.tvCommentCount.text = item.commentCount.moneyFormat() + + if (item.price < 1) { + binding.tvPrice.text = "무료" + binding.tvPrice.setCompoundDrawables(null, null, null, null) + } else { + binding.tvPrice.text = item.price.moneyFormat() + binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_coin_w, + 0, + 0, + 0 + ) + } + + binding.iv19.visibility = if (item.isAdult) { + View.VISIBLE + } else { + View.GONE + } + + binding.root.setOnClickListener { onClickItem(item.contentId) } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemAudioContentBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount() = items.count() +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentApi.kt new file mode 100644 index 0000000..2f8fe0e --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentApi.kt @@ -0,0 +1,140 @@ +package kr.co.vividnext.sodalive.audio_content + +import io.reactivex.rxjava3.core.Single +import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListResponse +import kr.co.vividnext.sodalive.audio_content.comment.RegisterAudioContentCommentRequest +import kr.co.vividnext.sodalive.audio_content.detail.GetAudioContentDetailResponse +import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest +import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeResponse +import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationRequest +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainResponse +import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListResponse +import kr.co.vividnext.sodalive.audio_content.order.OrderRequest +import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListResponse +import okhttp3.MultipartBody +import okhttp3.RequestBody +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Part +import retrofit2.http.Path +import retrofit2.http.Query + +interface AudioContentApi { + @GET("/audio-content") + fun getAudioContentList( + @Query("creator-id") id: Long, + @Query("page") page: Int, + @Query("size") size: Int, + @Query("sort-type") sort: AudioContentViewModel.Sort, + @Header("Authorization") authHeader: String + ): Single> + + @GET("/audio-content/theme") + fun getAudioContentThemeList( + @Header("Authorization") authHeader: String + ): Single>> + + @POST("/audio-content") + @Multipart + fun uploadAudioContent( + @Part coverImage: MultipartBody.Part, + @Part contentFile: MultipartBody.Part, + @Part("request") request: RequestBody, + @Header("Authorization") authHeader: String + ): Single> + + @GET("/audio-content/{id}") + fun getAudioContentDetail( + @Path("id") id: Long, + @Query("timezone") timezone: String, + @Header("Authorization") authHeader: String + ): Single> + + @POST("/order/audio-content") + fun orderAudioContent( + @Body request: OrderRequest, + @Header("Authorization") authHeader: String + ): Single> + + @GET("/order/audio-content") + fun getAudioContentOrderList( + @Query("page") page: Int, + @Query("size") size: Int, + @Header("Authorization") authHeader: String + ): Single> + + @POST("/audio-content/playback-tracking") + fun addAllPlaybackTracking( + @Body request: AddAllPlaybackTrackingRequest, + @Header("Authorization") authHeader: String + ): Single> + + @POST("/audio-content/comment") + fun registerComment( + @Body request: RegisterAudioContentCommentRequest, + @Header("Authorization") authHeader: String + ): Single> + + @GET("/audio-content/{id}/comment") + fun getAudioContentCommentList( + @Path("id") id: Long, + @Query("page") page: Int, + @Query("size") size: Int, + @Query("timezone") timezone: String, + @Header("Authorization") authHeader: String + ): Single> + + @GET("/audio-content/comment/{id}") + fun getAudioContentCommentCommentList( + @Path("id") id: Long, + @Query("page") page: Int, + @Query("size") size: Int, + @Query("timezone") timezone: String, + @Header("Authorization") authHeader: String + ): Single> + + @PUT("/audio-content/like") + fun likeContent( + @Body request: PutAudioContentLikeRequest, + @Header("Authorization") authHeader: String + ): Single> + + @PUT("/audio-content") + @Multipart + fun modifyAudioContent( + @Part coverImage: MultipartBody.Part?, + @Part("request") request: RequestBody, + @Header("Authorization") authHeader: String + ): Single> + + @DELETE("/audio-content/{id}") + fun deleteAudioContent( + @Path("id") id: Long, + @Header("Authorization") authHeader: String + ): Single> + + @GET("/audio-content/main") + fun getMain( + @Header("Authorization") authHeader: String + ): Single> + + @GET("/audio-content/main/new") + fun getNewContentOfTheme( + @Query("theme") theme: String, + @Header("Authorization") authHeader: String + ): Single>> + + @POST("/audio-content/donation") + fun donation( + @Body request: AudioContentDonationRequest, + @Header("Authorization") authHeader: String + ): Single> +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentPlayService.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentPlayService.kt new file mode 100644 index 0000000..3c6a25e --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentPlayService.kt @@ -0,0 +1,556 @@ +package kr.co.vividnext.sodalive.audio_content + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.media.AudioAttributes +import android.media.MediaPlayer +import android.os.Build +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import androidx.core.app.NotificationCompat +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.main.MainActivity +import org.koin.android.ext.android.inject + +class AudioContentPlayService : + Service(), + MediaPlayer.OnPreparedListener, + MediaPlayer.OnCompletionListener { + + private var playbackTrackingId: Long = 0 + private val playbackTrackingRepository: PlaybackTrackingRepository by inject() + + private lateinit var mediaPlayer: MediaPlayer + private var url: String? = null + private var title: String? = null + private var isFree: Boolean? = null + private var isPreview: Boolean? = null + private var nickname: String? = null + private var contentId: Long? = null + private var creatorId: Long? = null + private var coverImageUrl: String? = null + + private var isPlaying = false + + private val handler = Handler(Looper.getMainLooper()) + private var changeMediaPlayerPositionRunnable = object : Runnable { + override fun run() { + val intent = Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, mediaPlayer.currentPosition) + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId) + } + sendBroadcast(intent) + handler.postDelayed(this, 1000) + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + MusicAction.INIT.name -> { + val contentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0) + if (this.contentId != null && this.contentId == contentId) { + sendBroadcast( + Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER) + .apply { + putExtra( + Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI, + true + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_PLAYING, + isPlaying + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_DURATION, + mediaPlayer.duration + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_PROGRESS, + mediaPlayer.currentPosition + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_ID, + contentId + ) + } + ) + } + + if ( + this.contentId != null && + title != null && + nickname != null && + coverImageUrl != null + ) { + sendBroadcast( + Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER) + .apply { + putExtra( + Constants.EXTRA_AUDIO_CONTENT_SHOWING, + true + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_PLAYING, + isPlaying + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_TITLE, + title + ) + + putExtra( + Constants.EXTRA_NICKNAME, + nickname + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL, + coverImageUrl + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_ID, + this@AudioContentPlayService.contentId + ) + } + ) + } else { + sendBroadcast( + Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER) + .apply { + putExtra( + Constants.EXTRA_AUDIO_CONTENT_SHOWING, + false + ) + } + ) + } + } + + MusicAction.PLAY.name -> { + if (!isPlaying) { + mediaPlayer.start() + toggleIsPlaying() + updateNotification() + } + } + + MusicAction.PAUSE.name -> { + if (isPlaying) { + mediaPlayer.pause() + toggleIsPlaying() + updateNotification() + } + } + + MusicAction.STOP.name -> { + if (this::mediaPlayer.isInitialized) { + mediaPlayer.stop() + setEndPositionPlaybackTracking(mediaPlayer.currentPosition) + toggleIsPlaying(false) + onStopService() + } + } + + MusicAction.CONDITIONAL_STOP.name -> { + val contentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0) + if ( + this.contentId != null && + this.contentId == contentId && + this::mediaPlayer.isInitialized + ) { + mediaPlayer.stop() + setEndPositionPlaybackTracking(mediaPlayer.currentPosition) + toggleIsPlaying(false) + onStopService() + } + } + + MusicAction.PROGRESS.name -> { + val progress = intent.getIntExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, 0) + if (progress > 0) { + if (contentId != null) saveNewPlaybackTracking( + totalDuration = mediaPlayer.duration, + progress = progress + ) + mediaPlayer.seekTo(progress) + } + } + + else -> { + val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0) + if (contentId != null && this.contentId == contentId) { + if (isPlaying) { + sendBroadcast( + Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER) + .apply { + putExtra( + Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION, + MusicAction.PAUSE + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_ID, + contentId + ) + } + ) + } else { + sendBroadcast( + Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER) + .apply { + putExtra( + Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION, + MusicAction.PLAY + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_ID, + contentId + ) + } + ) + } + } else { + url = intent?.getStringExtra(Constants.EXTRA_AUDIO_CONTENT_URL) + title = intent?.getStringExtra(Constants.EXTRA_AUDIO_CONTENT_TITLE) + isFree = intent?.getBooleanExtra( + Constants.EXTRA_AUDIO_CONTENT_FREE, + true + ) + isPreview = intent?.getBooleanExtra( + Constants.EXTRA_AUDIO_CONTENT_PREVIEW, + true + ) + nickname = intent?.getStringExtra(Constants.EXTRA_NICKNAME) + coverImageUrl = intent?.getStringExtra( + Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL + ) + creatorId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, 0) + this.contentId = contentId + + if (url != null) { + sendBroadcast( + Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER) + .apply { + putExtra( + Constants.EXTRA_AUDIO_CONTENT_LOADING, + true + ) + } + ) + + if (isPlaying) { + mediaPlayer.stop() + setEndPositionPlaybackTracking(mediaPlayer.currentPosition) + + mediaPlayer.release() + toggleIsPlaying() + } + + initMediaPlayer() + mediaPlayer.setDataSource(url) + mediaPlayer.prepareAsync() + } + } + } + } + + return START_NOT_STICKY + } + + override fun onDestroy() { + if (this::mediaPlayer.isInitialized) { + mediaPlayer.release() + } + + onStopService() + super.onDestroy() + } + + override fun onBind(p0: Intent?): IBinder? { + return null + } + + override fun onCompletion(mp: MediaPlayer?) { + setEndPositionPlaybackTracking(mediaPlayer.currentPosition) + + if (SharedPreferenceManager.isContentPlayLoop) { + saveNewPlaybackTracking(totalDuration = mediaPlayer.duration, progress = 0) + mediaPlayer.start() + } else { + toggleIsPlaying(false) + mediaPlayer.release() + onStopService() + } + } + + private fun toggleIsPlaying(isPlaying: Boolean? = null) { + this.isPlaying = isPlaying ?: !this.isPlaying + if (this.isPlaying) { + handler.postDelayed(changeMediaPlayerPositionRunnable, 1000) + } else { + handler.removeCallbacks(changeMediaPlayerPositionRunnable) + } + + sendBroadcast( + Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER) + .apply { + putExtra( + Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI, + true + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_PLAYING, + this@AudioContentPlayService.isPlaying + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_ID, + contentId + ) + } + ) + + if (isPlaying != null && !isPlaying) { + resetAudioData() + } + + sendBroadcast( + Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER) + .apply { + putExtra( + Constants.EXTRA_AUDIO_CONTENT_PLAYING, + this@AudioContentPlayService.isPlaying + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_SHOWING, + contentId != null + ) + } + ) + } + + private fun resetAudioData() { + url = null + title = null + nickname = null + contentId = null + } + + private fun initMediaPlayer() { + mediaPlayer = MediaPlayer() + mediaPlayer.setOnPreparedListener(this) + mediaPlayer.setOnCompletionListener(this) + mediaPlayer.setAudioAttributes( + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .build() + ) + } + + override fun onPrepared(mp: MediaPlayer?) { + saveNewPlaybackTracking(totalDuration = mediaPlayer.duration, progress = 0) + sendBroadcast( + Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER) + .apply { + putExtra( + Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION, + MusicAction.PLAY + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_DURATION, + mediaPlayer.duration + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_ID, + contentId + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_ALERT_PREVIEW, + true + ) + } + ) + + sendBroadcast( + Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER) + .apply { + putExtra( + Constants.EXTRA_AUDIO_CONTENT_PLAYING, + false + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_SHOWING, + true + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_TITLE, + title + ) + + putExtra( + Constants.EXTRA_NICKNAME, + nickname + ) + + putExtra( + Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL, + coverImageUrl + ) + } + ) + } + + private fun updateNotification() { + val intent = Intent(this, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + val pendingIntent = PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT + ) + + val channelId = "audio_content_play_channel" + val notificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + channelId, + "콘텐츠 알림 채널", + NotificationManager.IMPORTANCE_LOW + ) + notificationManager.createNotificationChannel(channel) + } + + val playPauseIcon = + if (isPlaying) R.drawable.ic_noti_pause else R.drawable.ic_noti_play + val playPauseAction = + if (isPlaying) MusicAction.PAUSE.name else MusicAction.PLAY.name + + Glide + .with(this) + .asBitmap() + .load(coverImageUrl) + .into(object : CustomTarget() { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + val notificationBuilder = NotificationCompat + .Builder(this@AudioContentPlayService, channelId) + .setSmallIcon(R.drawable.ic_noti) + .setLargeIcon(resource) + .setContentTitle(title ?: "오디오 콘텐츠") + .setContentText(nickname ?: "") + .setContentIntent(pendingIntent) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .addAction( + NotificationCompat + .Action + .Builder( + playPauseIcon, + "Play_or_Pause", + getServiceIntent(playPauseAction) + ).build() + ) + .addAction( + NotificationCompat + .Action + .Builder( + R.drawable.ic_noti_stop, + "Stop", + getServiceIntent(MusicAction.STOP.name) + ).build() + ) + + notificationBuilder.setStyle( + androidx.media.app.NotificationCompat.MediaStyle() + .setShowActionsInCompactView(0, 1) + ) + + startForeground(1, notificationBuilder.build()) + } + + override fun onLoadCleared(placeholder: Drawable?) { + } + }) + } + + private fun getServiceIntent(action: String): PendingIntent { + val intent = Intent(this, AudioContentPlayService::class.java) + intent.action = action + return PendingIntent.getService( + this, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT + ) + } + + private fun saveNewPlaybackTracking(totalDuration: Int, progress: Int) { + if (creatorId != SharedPreferenceManager.userId) { + playbackTrackingId = playbackTrackingRepository.savePlaybackTracking( + PlaybackTracking( + contentId = contentId!!, + totalDuration = totalDuration, + startPosition = progress, + isFree = isFree!!, + isPreview = isPreview!! + ) + ) + } + } + + private fun setEndPositionPlaybackTracking(progress: Int) { + if (creatorId != SharedPreferenceManager.userId && playbackTrackingId > 0) { + val playbackTracking = playbackTrackingRepository + .getPlaybackTracking(playbackTrackingId) + + if (playbackTracking != null) { + playbackTracking.endPosition = progress + playbackTrackingRepository.savePlaybackTracking(playbackTracking) + } + + playbackTrackingId = 0 + } + } + + private fun onStopService() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_REMOVE) + } else { + stopForeground(true) + } + + stopSelf() + } + + enum class MusicAction { + PLAY, PAUSE, STOP, PROGRESS, INIT, CONDITIONAL_STOP + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentRepository.kt new file mode 100644 index 0000000..726edc4 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentRepository.kt @@ -0,0 +1,138 @@ +package kr.co.vividnext.sodalive.audio_content + +import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest +import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationRequest +import kr.co.vividnext.sodalive.audio_content.order.OrderRequest +import kr.co.vividnext.sodalive.audio_content.order.OrderType +import kr.co.vividnext.sodalive.user.CreatorFollowRequestRequest +import kr.co.vividnext.sodalive.user.UserApi +import okhttp3.MultipartBody +import okhttp3.RequestBody +import java.util.TimeZone + +class AudioContentRepository( + private val api: AudioContentApi, + private val userApi: UserApi +) { + fun getAudioContentList( + id: Long, + page: Int, + size: Int, + sort: AudioContentViewModel.Sort, + token: String + ) = api.getAudioContentList( + id = id, + page = page - 1, + size = size, + sort = sort, + authHeader = token + ) + + fun getAudioContentThemeList(token: String) = api.getAudioContentThemeList(token) + + fun uploadAudioContent( + coverImage: MultipartBody.Part, + contentFile: MultipartBody.Part, + request: RequestBody, + token: String + ) = api.uploadAudioContent( + coverImage = coverImage, + contentFile = contentFile, + request = request, + authHeader = token + ) + + fun modifyAudioContent( + coverImage: MultipartBody.Part? = null, + request: RequestBody, + token: String + ) = api.modifyAudioContent( + coverImage = coverImage, + request = request, + authHeader = token + ) + + fun deleteAudioContent( + id: Long, + token: String + ) = api.deleteAudioContent( + id = id, + authHeader = token + ) + + fun getAudioContentDetail(audioContentId: Long, token: String) = api.getAudioContentDetail( + id = audioContentId, + timezone = TimeZone.getDefault().id, + authHeader = token + ) + + fun registerNotification( + creatorId: Long, + token: String + ) = userApi.creatorFollow( + request = CreatorFollowRequestRequest(creatorId = creatorId), + authHeader = token + ) + + fun unRegisterNotification( + creatorId: Long, + token: String + ) = userApi.creatorUnFollow( + request = CreatorFollowRequestRequest(creatorId = creatorId), + authHeader = token + ) + + fun orderContent( + contentId: Long, + orderType: OrderType, + token: String + ) = api.orderAudioContent( + request = OrderRequest( + contentId = contentId, + orderType = orderType, + container = "aos" + ), + authHeader = token + ) + + fun getAudioContentOrderList( + page: Int, + size: Int, + token: String + ) = api.getAudioContentOrderList( + page = page - 1, + size = size, + authHeader = token + ) + + fun addAllPlaybackTracking( + request: AddAllPlaybackTrackingRequest, + token: String + ) = api.addAllPlaybackTracking(request, authHeader = token) + + fun likeContent( + request: PutAudioContentLikeRequest, + token: String + ) = api.likeContent(request, authHeader = token) + + fun getMain(token: String) = api.getMain(authHeader = token) + + fun getNewContentOfTheme(theme: String, token: String) = api.getNewContentOfTheme( + theme = theme, + authHeader = token + ) + + fun donation( + contentId: Long, + can: Int, + comment: String, + token: String + ) = api.donation( + request = AudioContentDonationRequest( + contentId = contentId, + donationCan = can, + comment = comment + ), + authHeader = token + ) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentViewModel.kt new file mode 100644 index 0000000..b9f1f8f --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentViewModel.kt @@ -0,0 +1,102 @@ +package kr.co.vividnext.sodalive.audio_content + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.google.gson.annotations.SerializedName +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListResponse + +class AudioContentViewModel(private val repository: AudioContentRepository) : BaseViewModel() { + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private var _audioContentListLiveData = MutableLiveData() + val audioContentListLiveData: LiveData + get() = _audioContentListLiveData + + private val _sort = MutableLiveData(Sort.NEWEST) + val sort: LiveData + get() = _sort + + enum class Sort { + @SerializedName("NEWEST") + NEWEST, + + @SerializedName("PRICE_HIGH") + PRICE_HIGH, + + @SerializedName("PRICE_LOW") + PRICE_LOW + } + + private var isLast = false + var page = 1 + private val size = 10 + + fun getAudioContentList(userId: Long, onFailure: (() -> Unit)? = null) { + if (!_isLoading.value!! && !isLast) { + _isLoading.value = true + compositeDisposable.add( + repository.getAudioContentList( + id = userId, + page = page, + size = size, + token = "Bearer ${SharedPreferenceManager.token}", + sort = _sort.value!! + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + if (it.data.items.isNotEmpty()) { + page += 1 + _audioContentListLiveData.postValue(it.data!!) + } else { + isLast = true + } + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + + if (onFailure != null) { + onFailure() + } + } + + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + if (onFailure != null) { + onFailure() + } + } + ) + ) + } + } + + fun changeSort(sort: Sort) { + page = 1 + isLast = false + _sort.postValue(sort) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/PlaybackTracking.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/PlaybackTracking.kt new file mode 100644 index 0000000..07b741f --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/PlaybackTracking.kt @@ -0,0 +1,23 @@ +package kr.co.vividnext.sodalive.audio_content + +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@Entity +data class PlaybackTracking( + @Id + var id: Long = 0, + var contentId: Long, + var totalDuration: Int, + var startPosition: Int, + var isFree: Boolean, + var isPreview: Boolean, + var endPosition: Int? = null, + var playDateTime: String = SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss", + Locale.getDefault() + ).format(Date()) +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/PlaybackTrackingRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/PlaybackTrackingRepository.kt new file mode 100644 index 0000000..905e224 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/PlaybackTrackingRepository.kt @@ -0,0 +1,29 @@ +package kr.co.vividnext.sodalive.audio_content + +import kr.co.vividnext.sodalive.common.ObjectBox + +class PlaybackTrackingRepository(private val objectBox: ObjectBox) { + fun savePlaybackTracking(data: PlaybackTracking): Long { + return objectBox.playbackTrackingBox.put(data) + } + + fun getPlaybackTracking(id: Long): PlaybackTracking? { + val query = objectBox.playbackTrackingBox + .query(PlaybackTracking_.id.equal(id)) + .build() + + val playbackTracking = query.findFirst() + query.close() + return playbackTracking + } + + fun getAllPlaybackTracking(): List { + return objectBox + .playbackTrackingBox + .all + } + + fun removeAllPlaybackTracking() { + objectBox.playbackTrackingBox.removeAll() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentAdapter.kt new file mode 100644 index 0000000..db7bb19 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentAdapter.kt @@ -0,0 +1,103 @@ +package kr.co.vividnext.sodalive.audio_content.comment + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.extensions.moneyFormat + +class AudioContentCommentAdapter( + private val onItemClick: (GetAudioContentCommentListItem) -> Unit +) : RecyclerView.Adapter() { + + var items = mutableSetOf() + + inner class ViewHolder( + private val binding: ItemAudioContentCommentBinding + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: GetAudioContentCommentListItem) { + binding.ivCommentProfile.load(item.profileUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + + val tvCommentLayoutParams = binding.tvComment.layoutParams as LinearLayout.LayoutParams + val coin = item.donationCoin + if (coin > 0) { + tvCommentLayoutParams.topMargin = 0 + binding.llDonationCoin.visibility = View.VISIBLE + binding.tvDonationCoin.text = coin.moneyFormat() + binding.llDonationCoin.setBackgroundResource( + when { + coin >= 100000 -> { + R.drawable.bg_round_corner_10_7_973a3a + } + + coin >= 50000 -> { + R.drawable.bg_round_corner_10_7_d85e37 + } + + coin >= 10000 -> { + R.drawable.bg_round_corner_10_7_d38c38 + } + + coin >= 5000 -> { + R.drawable.bg_round_corner_10_7_59548f + } + + coin >= 1000 -> { + R.drawable.bg_round_corner_10_7_4d6aa4 + } + + coin >= 500 -> { + R.drawable.bg_round_corner_10_7_2d7390 + } + + else -> { + R.drawable.bg_round_corner_10_7_548f7d + } + } + ) + } else { + tvCommentLayoutParams.topMargin = 13.3f.dpToPx().toInt() + binding.llDonationCoin.visibility = View.GONE + } + binding.tvComment.layoutParams = tvCommentLayoutParams + + binding.tvComment.text = item.comment + binding.tvCommentDate.text = item.date + binding.tvCommentNickname.text = item.nickname + + binding.tvWriteReply.text = if (item.replyCount > 0) { + "답글 ${item.replyCount}개" + } else { + "답글 쓰기" + } + + binding.tvWriteReply.setOnClickListener { onItemClick(item) } + binding.root.setOnClickListener { onItemClick(item) } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemAudioContentCommentBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items.toList()[position]) + } + + override fun getItemCount() = items.size +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentFragment.kt new file mode 100644 index 0000000..6cae097 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentFragment.kt @@ -0,0 +1,72 @@ +package kr.co.vividnext.sodalive.audio_content.comment + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.DialogAudioContentCommentBinding + +class AudioContentCommentFragment(private val audioContentId: Long) : BottomSheetDialogFragment() { + + private lateinit var binding: DialogAudioContentCommentBinding + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + + dialog.setOnShowListener { + val d = it as BottomSheetDialog + val bottomSheet = d.findViewById( + com.google.android.material.R.id.design_bottom_sheet + ) + if (bottomSheet != null) { + BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_EXPANDED + } + } + + return dialog + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = DialogAudioContentCommentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val commentListFragmentTag = "COMMENT_LIST_FRAGMENT" + val commentListFragment = AudioContentCommentListFragment.newInstance( + audioContentId = audioContentId + ) + val fragmentTransaction = childFragmentManager.beginTransaction() + fragmentTransaction.add(R.id.fl_container, commentListFragment, commentListFragmentTag) + fragmentTransaction.addToBackStack(commentListFragmentTag) + fragmentTransaction.commit() + } + + fun hideCommentDialog() { + dialog?.dismiss() + } + + fun onClickComment(comment: GetAudioContentCommentListItem) { + val commentReplyFragmentTag = "COMMENT_REPLY_FRAGMENT" + val commentReplyFragment = AudioContentCommentReplyFragment.newInstance( + audioContentId = audioContentId, + comment = comment + ) + val fragmentTransaction = childFragmentManager.beginTransaction() + fragmentTransaction.add(R.id.fl_container, commentReplyFragment, commentReplyFragmentTag) + fragmentTransaction.addToBackStack(commentReplyFragmentTag) + fragmentTransaction.commit() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentListFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentListFragment.kt new file mode 100644 index 0000000..3faaf15 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentListFragment.kt @@ -0,0 +1,182 @@ +package kr.co.vividnext.sodalive.audio_content.comment + +import android.annotation.SuppressLint +import android.app.Service +import android.graphics.Rect +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.base.BaseFragment +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.databinding.FragmentAudioContentCommentListBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject + +class AudioContentCommentListFragment : BaseFragment( + FragmentAudioContentCommentListBinding::inflate +) { + + private val viewModel: AudioContentCommentListViewModel by inject() + + private lateinit var imm: InputMethodManager + private lateinit var loadingDialog: LoadingDialog + private lateinit var adapter: AudioContentCommentAdapter + + private var audioContentId: Long = 0 + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0 + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + loadingDialog = LoadingDialog(requireActivity(), layoutInflater) + imm = requireContext().getSystemService( + Service.INPUT_METHOD_SERVICE + ) as InputMethodManager + + setupView() + bindData() + viewModel.getCommentList(audioContentId = audioContentId) { hideDialog() } + } + + private fun hideDialog() { + (parentFragment as AudioContentCommentFragment).hideCommentDialog() + } + + private fun setupView() { + binding.ivClose.setOnClickListener { hideDialog() } + + binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) { + crossfade(true) + placeholder(R.drawable.ic_logo) + transformations(CircleCropTransformation()) + } + + binding.ivCommentSend.setOnClickListener { + hideKeyboard() + val comment = binding.etComment.text.toString() + binding.etComment.setText("") + viewModel.registerComment(audioContentId, comment) + } + + adapter = AudioContentCommentAdapter { + (parentFragment as AudioContentCommentFragment).onClickComment(it) + } + + val recyclerView = binding.rvComment + recyclerView.setHasFixedSize(true) + recyclerView.layoutManager = LinearLayoutManager( + activity, + LinearLayoutManager.VERTICAL, + false + ) + recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + outRect.left = 13.3f.dpToPx().toInt() + outRect.right = 13.3f.dpToPx().toInt() + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.top = 13.3f.dpToPx().toInt() + outRect.bottom = 6.7f.dpToPx().toInt() + } + + adapter.itemCount - 1 -> { + outRect.top = 6.7f.dpToPx().toInt() + outRect.bottom = 13.3f.dpToPx().toInt() + } + + else -> { + outRect.top = 6.7f.dpToPx().toInt() + outRect.bottom = 6.7f.dpToPx().toInt() + } + } + } + }) + + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!! + .findLastCompletelyVisibleItemPosition() + val itemTotalCount = recyclerView.adapter!!.itemCount - 1 + + // 스크롤이 끝에 도달했는지 확인 + if (!recyclerView.canScrollVertically(1) && + lastVisibleItemPosition == itemTotalCount + ) { + viewModel.getCommentList(audioContentId = audioContentId) + } + } + }) + + recyclerView.adapter = adapter + } + + @SuppressLint("NotifyDataSetChanged") + private fun bindData() { + viewModel.isLoading.observe(viewLifecycleOwner) { + if (it) { + loadingDialog.show(screenWidth) + } else { + loadingDialog.dismiss() + } + } + + viewModel.toastLiveData.observe(viewLifecycleOwner) { + it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() } + } + + viewModel.totalCommentCount.observe(viewLifecycleOwner) { + binding.tvCommentCount.text = "$it" + } + + viewModel.commentList.observe(viewLifecycleOwner) { + if (viewModel.page - 1 == 1) { + adapter.items.clear() + binding.rvComment.scrollToPosition(0) + } + + adapter.items.addAll(it) + adapter.notifyDataSetChanged() + } + } + + private fun hideKeyboard() { + imm.hideSoftInputFromWindow(view?.windowToken, 0) + } + + companion object { + fun newInstance(audioContentId: Long): AudioContentCommentListFragment { + val args = Bundle() + args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId) + + val fragment = AudioContentCommentListFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentListViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentListViewModel.kt new file mode 100644 index 0000000..ece724a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentListViewModel.kt @@ -0,0 +1,125 @@ +package kr.co.vividnext.sodalive.audio_content.comment + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager + +class AudioContentCommentListViewModel( + private val repository: AudioContentCommentRepository +) : BaseViewModel() { + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private var _commentList = MutableLiveData>() + val commentList: LiveData> + get() = _commentList + + private var _totalCommentCount = MutableLiveData(0) + val totalCommentCount: LiveData + get() = _totalCommentCount + + var page = 1 + private var isLast = false + private val size = 10 + + fun getCommentList(audioContentId: Long, onFailure: (() -> Unit)? = null) { + if (!_isLoading.value!! && !isLast) { + _isLoading.value = true + compositeDisposable.add( + repository.getAudioContentCommentList( + audioContentId = audioContentId, + page = page, + size = size, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + _totalCommentCount.postValue(it.data.totalCount) + + if (it.data.items.isNotEmpty()) { + page += 1 + _commentList.postValue(it.data.items) + } else { + isLast = true + } + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + + if (onFailure != null) { + onFailure() + } + } + + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + if (onFailure != null) { + onFailure() + } + } + ) + ) + } + } + + fun registerComment(contentId: Long, comment: String) { + if (!_isLoading.value!!) { + _isLoading.value = true + } + + compositeDisposable.add( + repository.registerComment( + contentId = contentId, + comment = comment, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + + if (it.success) { + page = 1 + isLast = false + getCommentList(contentId) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyAdapter.kt new file mode 100644 index 0000000..8ef510a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyAdapter.kt @@ -0,0 +1,93 @@ +package kr.co.vividnext.sodalive.audio_content.comment + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import coil.load +import coil.transform.CircleCropTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentBinding +import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentReplyBinding + +class AudioContentCommentReplyAdapter : + RecyclerView.Adapter() { + + var items = mutableSetOf() + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): AudioContentCommentReplyViewHolder { + return if (viewType == 0) { + AudioContentCommentReplyHeaderViewHolder( + ItemAudioContentCommentBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } else { + AudioContentCommentReplyItemViewHolder( + ItemAudioContentCommentReplyBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + } + + override fun onBindViewHolder(holder: AudioContentCommentReplyViewHolder, position: Int) { + holder.bind(items.toList()[position]) + } + + override fun getItemCount() = items.size + + override fun getItemViewType(position: Int): Int { + return position + } +} + +abstract class AudioContentCommentReplyViewHolder( + binding: ViewBinding +) : RecyclerView.ViewHolder(binding.root) { + abstract fun bind(item: GetAudioContentCommentListItem) +} + +class AudioContentCommentReplyHeaderViewHolder( + private val binding: ItemAudioContentCommentBinding +) : AudioContentCommentReplyViewHolder(binding) { + + override fun bind(item: GetAudioContentCommentListItem) { + binding.ivCommentProfile.load(item.profileUrl) { + crossfade(true) + placeholder(R.drawable.ic_logo) + transformations(CircleCropTransformation()) + } + + binding.tvComment.text = item.comment + binding.tvCommentDate.text = item.date + binding.tvCommentNickname.text = item.nickname + + binding.tvWriteReply.visibility = View.GONE + } +} + +class AudioContentCommentReplyItemViewHolder( + private val binding: ItemAudioContentCommentReplyBinding +) : AudioContentCommentReplyViewHolder(binding) { + + override fun bind(item: GetAudioContentCommentListItem) { + binding.ivCommentProfile.load(item.profileUrl) { + crossfade(true) + placeholder(R.drawable.ic_logo) + transformations(CircleCropTransformation()) + } + + binding.tvComment.text = item.comment + binding.tvCommentDate.text = item.date + binding.tvCommentNickname.text = item.nickname + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyFragment.kt new file mode 100644 index 0000000..b3c6a94 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyFragment.kt @@ -0,0 +1,204 @@ +package kr.co.vividnext.sodalive.audio_content.comment + +import android.annotation.SuppressLint +import android.app.Service +import android.graphics.Rect +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import androidx.core.os.BundleCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.base.BaseFragment +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.databinding.FragmentAudioContentCommentReplyBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject + +class AudioContentCommentReplyFragment : BaseFragment( + FragmentAudioContentCommentReplyBinding::inflate +) { + + private val viewModel: AudioContentCommentReplyViewModel by inject() + + private lateinit var imm: InputMethodManager + private lateinit var loadingDialog: LoadingDialog + private lateinit var adapter: AudioContentCommentReplyAdapter + + private var originalComment: GetAudioContentCommentListItem? = null + private var audioContentId: Long = 0 + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0 + originalComment = BundleCompat.getParcelable( + requireArguments(), + Constants.EXTRA_AUDIO_CONTENT_COMMENT, + GetAudioContentCommentListItem::class.java + ) + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (originalComment == null) { + parentFragmentManager.popBackStack() + } + + loadingDialog = LoadingDialog(requireActivity(), layoutInflater) + imm = requireContext().getSystemService( + Service.INPUT_METHOD_SERVICE + ) as InputMethodManager + + setupView() + bindData() + viewModel.getCommentReplyList(commentId = originalComment!!.id) { + parentFragmentManager.popBackStack() + } + } + + private fun hideDialog() { + (parentFragment as AudioContentCommentFragment).hideCommentDialog() + } + + private fun setupView() { + binding.root.setOnClickListener { } + + binding.tvBack.setOnClickListener { + parentFragmentManager.popBackStack() + } + + binding.ivClose.setOnClickListener { hideDialog() } + + binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) { + crossfade(true) + placeholder(R.drawable.ic_logo) + transformations(CircleCropTransformation()) + } + + binding.ivCommentSend.setOnClickListener { + hideKeyboard() + val comment = binding.etComment.text.toString() + binding.etComment.setText("") + viewModel.registerComment(audioContentId, originalComment!!.id, comment) + } + + adapter = AudioContentCommentReplyAdapter().apply { + items.add(originalComment!!) + } + + val recyclerView = binding.rvCommentReply + recyclerView.setHasFixedSize(true) + recyclerView.layoutManager = LinearLayoutManager( + activity, + LinearLayoutManager.VERTICAL, + false + ) + + recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + outRect.left = 13.3f.dpToPx().toInt() + outRect.right = 13.3f.dpToPx().toInt() + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.top = 13.3f.dpToPx().toInt() + outRect.bottom = 12f.dpToPx().toInt() + } + + adapter.itemCount - 1 -> { + outRect.top = 12f.dpToPx().toInt() + outRect.bottom = 13.3f.dpToPx().toInt() + } + + else -> { + outRect.top = 12f.dpToPx().toInt() + outRect.bottom = 12f.dpToPx().toInt() + } + } + } + }) + + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!! + .findLastCompletelyVisibleItemPosition() + val itemTotalCount = recyclerView.adapter!!.itemCount - 1 + + // 스크롤이 끝에 도달했는지 확인 + if (!recyclerView.canScrollVertically(1) && + lastVisibleItemPosition == itemTotalCount + ) { + viewModel.getCommentReplyList(originalComment!!.id) + } + } + }) + + recyclerView.adapter = adapter + } + + @SuppressLint("NotifyDataSetChanged") + private fun bindData() { + viewModel.isLoading.observe(viewLifecycleOwner) { + if (it) { + loadingDialog.show(screenWidth) + } else { + loadingDialog.dismiss() + } + } + + viewModel.toastLiveData.observe(viewLifecycleOwner) { + it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() } + } + + viewModel.commentList.observe(viewLifecycleOwner) { + if (viewModel.page - 1 == 1) { + adapter.items.clear() + binding.rvCommentReply.scrollToPosition(0) + adapter.items.add(originalComment!!) + } + + adapter.items.addAll(it) + adapter.notifyDataSetChanged() + } + } + + private fun hideKeyboard() { + imm.hideSoftInputFromWindow(view?.windowToken, 0) + } + + companion object { + fun newInstance( + audioContentId: Long, + comment: GetAudioContentCommentListItem + ): AudioContentCommentReplyFragment { + val args = Bundle() + args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId) + args.putParcelable(Constants.EXTRA_AUDIO_CONTENT_COMMENT, comment) + + val fragment = AudioContentCommentReplyFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyViewModel.kt new file mode 100644 index 0000000..57e15ad --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentReplyViewModel.kt @@ -0,0 +1,120 @@ +package kr.co.vividnext.sodalive.audio_content.comment + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager + +class AudioContentCommentReplyViewModel( + private val repository: AudioContentCommentRepository +) : BaseViewModel() { + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private var _commentList = MutableLiveData>() + val commentList: LiveData> + get() = _commentList + + var page = 1 + private var isLast = false + private val size = 10 + + fun getCommentReplyList(commentId: Long, onFailure: (() -> Unit)? = null) { + if (!_isLoading.value!! && !isLast) { + _isLoading.value = true + compositeDisposable.add( + repository.getAudioContentCommentReplyList( + commentId = commentId, + page = page, + size = size, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + if (it.data.items.isNotEmpty()) { + page += 1 + _commentList.postValue(it.data.items) + } else { + isLast = true + } + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + + if (onFailure != null) { + onFailure() + } + } + + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + if (onFailure != null) { + onFailure() + } + } + ) + ) + } + } + + fun registerComment(contentId: Long, commentId: Long, comment: String) { + if (!_isLoading.value!!) { + _isLoading.value = true + } + + compositeDisposable.add( + repository.registerComment( + contentId = contentId, + comment = comment, + parentId = commentId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + + if (it.success) { + page = 1 + isLast = false + getCommentReplyList(commentId) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentRepository.kt new file mode 100644 index 0000000..ad010c7 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/AudioContentCommentRepository.kt @@ -0,0 +1,46 @@ +package kr.co.vividnext.sodalive.audio_content.comment + +import kr.co.vividnext.sodalive.audio_content.AudioContentApi +import java.util.TimeZone + +class AudioContentCommentRepository(private val api: AudioContentApi) { + fun registerComment( + contentId: Long, + comment: String, + parentId: Long? = null, + token: String + ) = api.registerComment( + request = RegisterAudioContentCommentRequest( + comment = comment, + contentId = contentId, + parentId = parentId + ), + authHeader = token + ) + + fun getAudioContentCommentList( + audioContentId: Long, + page: Int, + size: Int, + token: String + ) = api.getAudioContentCommentList( + id = audioContentId, + page = page - 1, + size = size, + timezone = TimeZone.getDefault().id, + authHeader = token + ) + + fun getAudioContentCommentReplyList( + commentId: Long, + page: Int, + size: Int, + token: String + ) = api.getAudioContentCommentCommentList( + id = commentId, + page = page - 1, + size = size, + timezone = TimeZone.getDefault().id, + authHeader = token + ) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/GetAudioContentCommentListResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/GetAudioContentCommentListResponse.kt new file mode 100644 index 0000000..288deff --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/GetAudioContentCommentListResponse.kt @@ -0,0 +1,21 @@ +package kr.co.vividnext.sodalive.audio_content.comment + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +data class GetAudioContentCommentListResponse( + @SerializedName("totalCount") val totalCount: Int, + @SerializedName("items") val items: List +) + +@Parcelize +data class GetAudioContentCommentListItem( + @SerializedName("id") val id: Long, + @SerializedName("nickname") val nickname: String, + @SerializedName("profileUrl") val profileUrl: String, + @SerializedName("comment") val comment: String, + @SerializedName("donationCoin") val donationCoin: Int, + @SerializedName("date") val date: String, + @SerializedName("replyCount") val replyCount: Int +) : Parcelable diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/RegisterAudioContentCommentRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/RegisterAudioContentCommentRequest.kt new file mode 100644 index 0000000..1c3f093 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/comment/RegisterAudioContentCommentRequest.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.audio_content.comment + +import com.google.gson.annotations.SerializedName + +data class RegisterAudioContentCommentRequest( + @SerializedName("comment") val comment: String, + @SerializedName("contentId") val contentId: Long, + @SerializedName("parentId") val parentId: Long? +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDeleteDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDeleteDialog.kt new file mode 100644 index 0000000..a58d53b --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDeleteDialog.kt @@ -0,0 +1,67 @@ +package kr.co.vividnext.sodalive.audio_content.detail + +import android.annotation.SuppressLint +import android.app.Activity +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.WindowManager +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import kr.co.vividnext.sodalive.databinding.DialogAudioContentDeleteBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +@SuppressLint("SetTextI18n") +class AudioContentDeleteDialog( + activity: Activity, + layoutInflater: LayoutInflater, + title: String, + confirmButtonClick: () -> Unit +) { + + private val alertDialog: AlertDialog + + val dialogView = DialogAudioContentDeleteBinding.inflate(layoutInflater) + + init { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder.setView(dialogView.root) + + alertDialog = dialogBuilder.create() + alertDialog.setCancelable(false) + alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + dialogView.tvTitle.text = "[$title]을 삭제하시겠습니까?" + dialogView.tvCancel.setOnClickListener { + alertDialog.dismiss() + } + + dialogView.tvConfirm.setOnClickListener { + if (dialogView.tvNotice.isSelected) { + alertDialog.dismiss() + confirmButtonClick() + } else { + Toast.makeText( + activity, + "동의하셔야 삭제할 수 있습니다.", + Toast.LENGTH_LONG + ).show() + } + } + + dialogView.tvNotice.setOnClickListener { + it.isSelected = !it.isSelected + } + } + + fun show(width: Int) { + alertDialog.show() + + val lp = WindowManager.LayoutParams() + lp.copyFrom(alertDialog.window?.attributes) + lp.width = width - (26.7f.dpToPx()).toInt() + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + alertDialog.window?.attributes = lp + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt new file mode 100644 index 0000000..29e0722 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt @@ -0,0 +1,788 @@ +package kr.co.vividnext.sodalive.audio_content.detail + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Rect +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.widget.RelativeLayout +import android.widget.SeekBar +import android.widget.Toast +import androidx.appcompat.widget.PopupMenu +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions +import com.google.gson.Gson +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService +import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentFragment +import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyActivity +import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderConfirmDialog +import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderFragment +import kr.co.vividnext.sodalive.audio_content.order.OrderType +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.common.Utils +import kr.co.vividnext.sodalive.databinding.ActivityAudioContentDetailBinding +import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog +import kr.co.vividnext.sodalive.mypage.auth.Auth +import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest +import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse +import kr.co.vividnext.sodalive.report.ReportType +import org.koin.android.ext.android.inject + +class AudioContentDetailActivity : BaseActivity( + ActivityAudioContentDetailBinding::inflate +) { + private val viewModel: AudioContentDetailViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + private lateinit var creatorOtherContentAdapter: OtherContentAdapter + private lateinit var sameThemeOtherContentAdapter: OtherContentAdapter + + private var audioContentId: Long = 0 + private var isAlertPreview = false + private val audioContentReceiver = AudioContentReceiver() + + private var refresh = false + set(value) { + field = value + setResult(RESULT_OK) + } + private var title = "" + + @SuppressLint("SetTextI18n") + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + + binding.scrollView.scrollTo(0, 0) + binding.sbProgress.progress = 0 + binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_play) + binding.tvTotalDuration.text = " / 00:00:00" + binding.tvCurrentDuration.text = "00:00:00" + binding.rlPreviewAlert.visibility = View.GONE + + audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0) + viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() } + } + + override fun onCreate(savedInstanceState: Bundle?) { + audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0) + super.onCreate(savedInstanceState) + + if (audioContentId <= 0) { + Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show() + finish() + } + + bindData() + viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() } + } + + override fun onResume() { + super.onResume() + val intentFilter = IntentFilter(Constants.ACTION_AUDIO_CONTENT_RECEIVER) + registerReceiver(audioContentReceiver, intentFilter) + + if (refresh) { + viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() } + } + } + + override fun onPause() { + super.onPause() + unregisterReceiver(audioContentReceiver) + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + binding.tvBack.text = "콘텐츠 상세" + binding.tvBack.setOnClickListener { finish() } + binding.ivClosePreviewAlert.setOnClickListener { viewModel.toggleShowPreviewAlert() } + binding.ivMenu.setOnClickListener { + showOptionMenu( + this, + binding.ivMenu, + ) + } + + creatorOtherContentAdapter = OtherContentAdapter { + val intent = Intent(applicationContext, AudioContentDetailActivity::class.java) + .apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + startActivity(intent) + } + + binding.rvCreatorOtherContent.layoutManager = LinearLayoutManager( + applicationContext, + LinearLayoutManager.HORIZONTAL, + false + ) + + binding.swipeRefreshLayout.setOnRefreshListener { + viewModel.getAudioContentDetail( + audioContentId = audioContentId + ) { finish() } + binding.swipeRefreshLayout.isRefreshing = false + } + + binding.rvCreatorOtherContent.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.left = 0 + outRect.right = 6.7f.dpToPx().toInt() + } + + creatorOtherContentAdapter.itemCount - 1 -> { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 0 + } + + else -> { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 6.7f.dpToPx().toInt() + } + } + } + }) + + binding.rvCreatorOtherContent.adapter = creatorOtherContentAdapter + + sameThemeOtherContentAdapter = OtherContentAdapter { + val intent = Intent(applicationContext, AudioContentDetailActivity::class.java) + .apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + startActivity(intent) + } + + binding.rvThemeOtherContent.layoutManager = LinearLayoutManager( + applicationContext, + LinearLayoutManager.HORIZONTAL, + false + ) + + binding.rvThemeOtherContent.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.left = 0 + outRect.right = 6.7f.dpToPx().toInt() + } + + creatorOtherContentAdapter.itemCount - 1 -> { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 0 + } + + else -> { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 6.7f.dpToPx().toInt() + } + } + } + }) + + binding.rvThemeOtherContent.adapter = sameThemeOtherContentAdapter + + binding.sbProgress.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + } + + override fun onStartTrackingTouch(p0: SeekBar?) { + } + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + if (seekBar != null) { + val intent = Intent( + this@AudioContentDetailActivity, + AudioContentPlayService::class.java + ) + intent.action = AudioContentPlayService.MusicAction.PROGRESS.name + intent.putExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, seekBar.progress) + startService(intent) + } + } + }) + + val layoutParams = binding.ivCover.layoutParams as RelativeLayout.LayoutParams + layoutParams.width = (screenWidth - 13.3f.dpToPx()).toInt() + layoutParams.height = (screenWidth - 13.3f.dpToPx()).toInt() + binding.ivCover.layoutParams = layoutParams + binding.ivPlayLoop.setOnClickListener { viewModel.togglePlayLoop() } + binding.llDonation.setOnClickListener { + val dialog = LiveRoomDonationDialog( + this, + LayoutInflater.from(this) + ) { can, message -> + if (can <= 0) { + showToast("1코인 이상 후원하실 수 있습니다.") + } else if (message.isBlank()) { + showToast("함께 보낼 메시지를 입력하세요.") + } else { + donation(can, message) + } + } + + dialog.show(screenWidth) + } + } + + private fun donation(coin: Int, message: String) { + viewModel.donation(audioContentId, coin, message) { + viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() } + } + } + + private fun showOptionMenu(context: Context, v: View) { + val popup = PopupMenu(context, v) + val inflater = popup.menuInflater + + if ( + viewModel.audioContentLiveData.value!!.creator.creatorId == + SharedPreferenceManager.userId + ) { + inflater.inflate(R.menu.audio_content_detail_creator_menu, popup.menu) + + popup.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_modify -> { + refresh = true + startActivity( + Intent(applicationContext, AudioContentModifyActivity::class.java) + .apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId) + } + ) + } + + R.id.menu_delete -> { + showDeleteDialog() + } + } + + true + } + } else { + inflater.inflate(R.menu.audio_content_detail_user_menu, popup.menu) + + popup.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_report -> { + showReportDialog() + } + } + + true + } + } + + popup.show() + } + + private fun showDeleteDialog() { + AudioContentDeleteDialog( + this, + layoutInflater, + this.title, + confirmButtonClick = { + viewModel.deleteAudioContent(audioContentId) { + setResult(RESULT_OK) + finish() + } + } + ).show(screenWidth) + } + + private fun showReportDialog() { + AudioContentReportDialog(this, layoutInflater) { + viewModel.report( + type = ReportType.AUDIO_CONTENT, + contentId = audioContentId, + reason = it + ) + }.show(screenWidth) + } + + @SuppressLint("NotifyDataSetChanged", "SetTextI18n") + private fun bindData() { + viewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + viewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth, "") + } else { + loadingDialog.dismiss() + } + } + + viewModel.isExpandDetail.observe(this) { + binding.tvDetail.maxLines = if (it) { + Int.MAX_VALUE + } else { + 2 + } + } + + viewModel.isShowPreviewAlert.observe(this) { + binding.rlPreviewAlert.visibility = if (it) { + View.VISIBLE + } else { + View.GONE + } + } + + viewModel.audioContentLiveData.observe(this) { + refresh = false + startService( + Intent(this, AudioContentPlayService::class.java).apply { + action = AudioContentPlayService.MusicAction.INIT.name + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it.contentId) + } + ) + + title = it.title + setupCreatorArea(it.creator) + setupMosaicArea(it.isMosaic) + setupPlayArea(it) + setupInfoArea(it) + setupPurchaseButton(it) + setupCommentArea(it) + setupCreatorOtherContentListArea(it.creatorOtherContentList) + setupSameThemeOtherContentList(it.sameThemeOtherContentList) + + isAlertPreview = it.creator.creatorId != SharedPreferenceManager.userId && + !it.existOrdered && + it.price > 0 + } + + viewModel.isContentPlayLoopLiveData.observe(this) { + if (it) { + binding.ivPlayLoop.setImageResource(R.drawable.btn_player_repeat) + } else { + binding.ivPlayLoop.setImageResource(R.drawable.btn_player_repeat_done) + } + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun setupSameThemeOtherContentList( + sameThemeOtherContentList: List + ) { + if (sameThemeOtherContentList.isEmpty()) { + binding.rvThemeOtherContent.visibility = View.GONE + binding.llThemeOtherContentPreparing.visibility = View.VISIBLE + } else { + binding.rvThemeOtherContent.visibility = View.VISIBLE + binding.llThemeOtherContentPreparing.visibility = View.GONE + + sameThemeOtherContentAdapter.items.clear() + sameThemeOtherContentAdapter.items.addAll(sameThemeOtherContentList) + sameThemeOtherContentAdapter.notifyDataSetChanged() + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun setupCreatorOtherContentListArea( + creatorOtherContentList: List + ) { + if (creatorOtherContentList.isEmpty()) { + binding.rvCreatorOtherContent.visibility = View.GONE + binding.llCreatorOtherContentPreparing.visibility = View.VISIBLE + } else { + binding.rvCreatorOtherContent.visibility = View.VISIBLE + binding.llCreatorOtherContentPreparing.visibility = View.GONE + + creatorOtherContentAdapter.items.clear() + creatorOtherContentAdapter.items.addAll(creatorOtherContentList) + creatorOtherContentAdapter.notifyDataSetChanged() + } + } + + private fun setupCommentArea(response: GetAudioContentDetailResponse) { + if (response.isCommentAvailable) { + binding.llDonation.visibility = View.VISIBLE + binding.llComment.visibility = View.VISIBLE + binding.tvCommentCount.text = "${response.commentCount}" + + if (response.commentCount > 0) { + binding.ivCommentProfile.load(response.commentList[0].profileUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + binding.tvCommentText.text = response.commentList[0].comment + binding.tvCommentText.visibility = View.VISIBLE + binding.rlInputComment.visibility = View.GONE + + binding.llComment.setOnClickListener { showCommentBottomSheetDialog() } + } else { + binding.tvCommentText.visibility = View.GONE + binding.rlInputComment.visibility = View.VISIBLE + binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + + binding.ivCommentSend.setOnClickListener { + val comment = binding.etComment.text.toString() + binding.etComment.setText("") + viewModel.registerComment(audioContentId, comment) + } + + binding.llComment.setOnClickListener {} + } + } else { + binding.llComment.visibility = View.GONE + binding.llDonation.visibility = View.GONE + } + } + + private fun showCommentBottomSheetDialog() { + val dialog = AudioContentCommentFragment(audioContentId = audioContentId) + dialog.show( + supportFragmentManager, + dialog.tag + ) + } + + private fun setupPurchaseButton(response: GetAudioContentDetailResponse) { + if ( + response.price > 0 && + !response.existOrdered && + response.orderType == null && + response.creator.creatorId != SharedPreferenceManager.userId + ) { + binding.llPurchase.visibility = View.VISIBLE + binding.tvPrice.text = response.price.toString() + + binding.llPurchase.setOnClickListener { + showOrderDialog(audioContent = response) + } + } else { + binding.llPurchase.visibility = View.GONE + } + } + + @SuppressLint("SetTextI18n") + private fun setupPlayArea(response: GetAudioContentDetailResponse) { + Glide + .with(this) + .load(response.coverImageUrl) + .centerCrop() + .placeholder(R.drawable.bg_black) + .apply(RequestOptions().override((screenWidth - 13.3f.dpToPx()).toInt())) + .into(binding.ivCover) + + binding.ivPlayOrPause.setOnClickListener { + startService( + Intent(this, AudioContentPlayService::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL, response.coverImageUrl) + putExtra(Constants.EXTRA_AUDIO_CONTENT_URL, response.contentUrl) + putExtra(Constants.EXTRA_NICKNAME, response.creator.nickname) + putExtra(Constants.EXTRA_AUDIO_CONTENT_TITLE, response.title) + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, response.contentId) + putExtra(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, response.creator.creatorId) + putExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, response.price <= 0) + putExtra( + Constants.EXTRA_AUDIO_CONTENT_PREVIEW, + !response.existOrdered && response.price > 0 + ) + } + ) + } + + binding.tvTotalDuration.text = " / ${response.duration}" + } + + @SuppressLint("SetTextI18n") + private fun setupInfoArea(response: GetAudioContentDetailResponse) { + binding.tvTheme.text = response.themeStr + binding.tv19.visibility = if (response.isAdult) { + View.VISIBLE + } else { + View.GONE + } + + if (response.orderType != null && response.orderType == OrderType.KEEP) { + binding.tvPurchased.visibility = View.VISIBLE + binding.tvRental.visibility = View.GONE + binding.tvRemainingTime.visibility = View.GONE + } else if (response.orderType != null && response.orderType == OrderType.RENTAL) { + binding.tvPurchased.visibility = View.GONE + binding.tvRental.visibility = View.VISIBLE + binding.tvRemainingTime.visibility = View.VISIBLE + binding.tvRemainingTime.text = response.remainingTime + } else { + binding.tvPurchased.visibility = View.GONE + binding.tvRental.visibility = View.GONE + binding.tvRemainingTime.visibility = View.GONE + } + + binding.tvTitle.text = response.title + binding.tvDetail.text = response.detail + binding.tvDetail.setOnClickListener { viewModel.toggleExpandDetail() } + + if (response.tag.isNotBlank()) { + binding.tvTag.visibility = View.VISIBLE + binding.tvTag.text = response.tag + } else { + binding.tvTag.visibility = View.GONE + } + + binding.ivLike.setImageResource( + if (response.isLike) { + R.drawable.ic_audio_content_heart_pressed + } else { + R.drawable.ic_audio_content_heart_normal + } + ) + + binding.tvLike.text = "${response.likeCount}" + binding.llLike.setOnClickListener { + viewModel.likeContent(contentId = audioContentId) { + val likeCount = binding.tvLike.text.toString().toInt() + if (it) { + binding.tvLike.text = "${likeCount + 1}" + binding.ivLike.setImageResource(R.drawable.ic_audio_content_heart_pressed) + } else { + binding.tvLike.text = if (likeCount - 1 < 0) { + "0" + } else { + "${likeCount - 1}" + } + binding.ivLike.setImageResource(R.drawable.ic_audio_content_heart_normal) + } + } + } + + binding.tvShare.setOnClickListener { + viewModel.shareAudioContent( + audioContentId = audioContentId, + contentImage = response.coverImageUrl, + contentTitle = "${response.title} - ${response.creator.nickname}" + ) { + val intent = Intent(Intent.ACTION_SEND) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TEXT, it) + + val shareIntent = Intent.createChooser(intent, "오디오콘텐츠 공유") + startActivity(shareIntent) + } + } + } + + private fun setupMosaicArea(isMosaic: Boolean) { + if (isMosaic) { + binding.alert19Bg.visibility = View.VISIBLE + binding.llAlert19.visibility = View.VISIBLE + + binding.tvAuth.setOnClickListener { + Auth.auth(this, applicationContext) { data -> + val bootpayResponse = Gson().fromJson(data, BootpayResponse::class.java) + val request = AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId) + runOnUiThread { + viewModel.authVerify(audioContentId = audioContentId, request = request) + } + } + } + } else { + binding.alert19Bg.visibility = View.GONE + binding.llAlert19.visibility = View.GONE + binding.tvAuth.setOnClickListener {} + } + } + + private fun setupCreatorArea(creator: AudioContentCreator) { + binding.rlProfile.setOnClickListener { + startActivity( + Intent(applicationContext, UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, creator.creatorId) + } + ) + } + + binding.ivProfile.load(creator.profileImageUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + + binding.tvProfileNickname.text = creator.nickname + + if (creator.creatorId != SharedPreferenceManager.userId) { + binding.ivFollow.visibility = View.VISIBLE + + if (creator.isFollowing) { + binding.ivFollow.setImageResource(R.drawable.btn_notification_selected) + binding.ivFollow.setOnClickListener { + viewModel.unRegisterNotification( + contentId = audioContentId, + creatorId = creator.creatorId + ) + } + } else { + binding.ivFollow.setImageResource(R.drawable.btn_notification) + binding.ivFollow.setOnClickListener { + viewModel.registerNotification( + contentId = audioContentId, + creatorId = creator.creatorId + ) + } + } + } else { + binding.ivFollow.visibility = View.GONE + } + } + + private fun showOrderDialog(audioContent: GetAudioContentDetailResponse) { + val dialog = AudioContentOrderFragment( + price = audioContent.price, + onClickKeep = { showOrderConfirmDialog(audioContent, OrderType.KEEP) }, + onClickRental = { showOrderConfirmDialog(audioContent, OrderType.RENTAL) } + ) + + dialog.show( + supportFragmentManager, + dialog.tag + ) + } + + private fun showOrderConfirmDialog( + audioContent: GetAudioContentDetailResponse, + orderType: OrderType + ) { + AudioContentOrderConfirmDialog( + activity = this, + layoutInflater = layoutInflater, + title = audioContent.title, + theme = audioContent.themeStr, + coverImageUrl = audioContent.coverImageUrl, + isAdult = audioContent.isAdult, + profileImageUrl = audioContent.creator.profileImageUrl, + nickname = audioContent.creator.nickname, + duration = audioContent.duration, + orderType = orderType, + price = audioContent.price, + confirmButtonClick = { + startService( + Intent(this, AudioContentPlayService::class.java).apply { + action = AudioContentPlayService.MusicAction.CONDITIONAL_STOP.name + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContent.contentId) + } + ) + + binding.rlPreviewAlert.visibility = View.GONE + + viewModel.order( + contentId = audioContent.contentId, + orderType = orderType + ) + }, + ).show(screenWidth) + } + + inner class AudioContentReceiver : BroadcastReceiver() { + @SuppressLint("SetTextI18n") + override fun onReceive(context: Context?, intent: Intent?) { + val nextAction = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent?.getSerializableExtra( + Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION, + AudioContentPlayService.MusicAction::class.java + ) + } else { + intent?.getSerializableExtra(Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION) + } + + val duration = intent?.getIntExtra(Constants.EXTRA_AUDIO_CONTENT_DURATION, 0) + val progress = intent?.getIntExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, 0) + val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0) + val isPlaying = intent?.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYING, false) + val changeUi = intent?.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI, false) + val alertPreview = intent?.getBooleanExtra( + Constants.EXTRA_AUDIO_CONTENT_ALERT_PREVIEW, + false + ) + val isLoading = intent?.getBooleanExtra( + Constants.EXTRA_AUDIO_CONTENT_LOADING, + false + ) + + viewModel.isLoading.value = isLoading ?: false + + if (this@AudioContentDetailActivity.audioContentId == contentId) { + runOnUiThread { + if (changeUi != null && changeUi) { + binding.ivPlayOrPause.setImageResource( + if (isPlaying != null && isPlaying) { + R.drawable.btn_audio_content_pause + } else { + R.drawable.btn_audio_content_play + } + ) + } + } + + if (duration != null && duration > 0) { + binding.sbProgress.max = duration + binding.tvTotalDuration.text = " / ${Utils.convertDurationToString(duration)}" + } + + if (progress != null && progress > 0) { + binding.sbProgress.progress = progress + binding.tvCurrentDuration.text = Utils.convertDurationToString(progress) + } + + if (alertPreview != null && alertPreview && isAlertPreview) { + viewModel.toggleShowPreviewAlert() + } + + if (nextAction != null) { + startService( + Intent( + this@AudioContentDetailActivity, + AudioContentPlayService::class.java + ).apply { + action = (nextAction as AudioContentPlayService.MusicAction).name + } + ) + } + } + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailViewModel.kt new file mode 100644 index 0000000..313bfac --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailViewModel.kt @@ -0,0 +1,475 @@ +package kr.co.vividnext.sodalive.audio_content.detail + +import android.net.Uri +import androidx.core.net.toUri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.google.firebase.dynamiclinks.ShortDynamicLink +import com.google.firebase.dynamiclinks.ktx.androidParameters +import com.google.firebase.dynamiclinks.ktx.dynamicLinks +import com.google.firebase.dynamiclinks.ktx.iosParameters +import com.google.firebase.dynamiclinks.ktx.shortLinkAsync +import com.google.firebase.dynamiclinks.ktx.socialMetaTagParameters +import com.google.firebase.ktx.Firebase +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.audio_content.AudioContentRepository +import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentRepository +import kr.co.vividnext.sodalive.audio_content.order.OrderType +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.extensions.moneyFormat +import kr.co.vividnext.sodalive.mypage.auth.AuthRepository +import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest +import kr.co.vividnext.sodalive.report.ReportRepository +import kr.co.vividnext.sodalive.report.ReportRequest +import kr.co.vividnext.sodalive.report.ReportType + +class AudioContentDetailViewModel( + private val repository: AudioContentRepository, + private val authRepository: AuthRepository, + private val reportRepository: ReportRepository, + private val commentRepository: AudioContentCommentRepository +) : BaseViewModel() { + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + var isLoading = MutableLiveData(false) + private set + + private var _audioContentLiveData = MutableLiveData() + val audioContentLiveData: LiveData + get() = _audioContentLiveData + + private val _isExpandDetail = MutableLiveData(false) + val isExpandDetail: LiveData + get() = _isExpandDetail + + private val _isShowPreviewAlert = MutableLiveData(false) + val isShowPreviewAlert: LiveData + get() = _isShowPreviewAlert + + private val _isContentPlayLoopLiveData = MutableLiveData( + SharedPreferenceManager.isContentPlayLoop + ) + val isContentPlayLoopLiveData: LiveData + get() = _isContentPlayLoopLiveData + + fun getAudioContentDetail(audioContentId: Long, onFailure: (() -> Unit)? = null) { + if (!isLoading.value!!) { + isLoading.value = true + } + + compositeDisposable.add( + repository.getAudioContentDetail( + audioContentId = audioContentId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + _audioContentLiveData.postValue(it.data!!) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + + if (onFailure != null) { + onFailure() + } + } + + isLoading.value = false + }, + { + isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + if (onFailure != null) { + onFailure() + } + } + ) + ) + } + + fun registerNotification(contentId: Long, creatorId: Long) { + isLoading.value = true + compositeDisposable.add( + repository.registerNotification( + creatorId, + "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + getAudioContentDetail(contentId) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + isLoading.value = false + }, + { + isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun unRegisterNotification(contentId: Long, creatorId: Long) { + isLoading.value = true + compositeDisposable.add( + repository.unRegisterNotification( + creatorId, + "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + getAudioContentDetail(contentId) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + isLoading.value = false + }, + { + isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun toggleExpandDetail() { + _isExpandDetail.value = !_isExpandDetail.value!! + } + + fun toggleShowPreviewAlert() { + _isShowPreviewAlert.value = !_isShowPreviewAlert.value!! + } + + fun order(contentId: Long, orderType: OrderType) { + isLoading.value = true + compositeDisposable.add( + repository.orderContent( + contentId = contentId, + orderType = orderType, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + getAudioContentDetail(audioContentId = contentId) + _toastLiveData.postValue("구매가 완료되었습니다.") + } 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 authVerify(audioContentId: Long, request: AuthVerifyRequest) { + if (!isLoading.value!!) { + isLoading.value = true + } + + compositeDisposable.add( + authRepository.verify(request, "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success) { + getAudioContentDetail(audioContentId) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + + isLoading.value = false + } + }, + { + isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun registerComment(audioContentId: Long, comment: String) { + if (!isLoading.value!!) { + isLoading.value = true + } + + compositeDisposable.add( + commentRepository.registerComment( + contentId = audioContentId, + comment = comment, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success) { + getAudioContentDetail(audioContentId) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + + isLoading.value = false + } + }, + { + isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun likeContent(contentId: Long, onSuccess: (Boolean) -> Unit) { + if (!isLoading.value!!) { + isLoading.value = true + } + + compositeDisposable.add( + repository.likeContent( + request = PutAudioContentLikeRequest(contentId = contentId), + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + isLoading.value = false + + if (it.success) { + if (it.data != null) { + onSuccess(it.data.like) + } else { + getAudioContentDetail(contentId) + } + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun shareAudioContent( + audioContentId: Long, + contentImage: String, + contentTitle: String, + onSuccess: (String) -> Unit + ) { + isLoading.value = true + Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) { + link = Uri.parse("https://yozm.day/?audio_content_id=$audioContentId") + domainUriPrefix = "https://yozm.page.link" + androidParameters { } + iosParameters("kr.co.vividnext.yozm") { + appStoreId = "1630284226" + } + socialMetaTagParameters { + title = contentTitle + description = "지금 요즘라이브에서 이 콘텐츠 감상하기" + imageUrl = contentImage.toUri() + } + }.addOnSuccessListener { + val uri = it.shortLink + if (uri != null) { + val message = uri.toString() + onSuccess(message) + } else { + _toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.") + } + }.addOnFailureListener { + _toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.") + }.addOnCompleteListener { + isLoading.value = false + } + } + + fun deleteAudioContent(audioContentId: Long, onSuccess: () -> Unit) { + isLoading.value = true + + compositeDisposable.add( + repository.deleteAudioContent( + audioContentId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + isLoading.value = false + + if (it.success) { + _toastLiveData.postValue( + "삭제되었습니다." + ) + onSuccess() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun report(type: ReportType, contentId: Long, reason: String) { + isLoading.value = true + val request = ReportRequest(type = type, reason = reason, contentId = contentId) + compositeDisposable.add( + reportRepository.report( + request = request, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "신고가 접수되었습니다." + ) + } + + isLoading.value = false + }, + { + isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("신고가 접수되었습니다.") + } + ) + ) + } + + fun togglePlayLoop() { + val isPlayLoop = !SharedPreferenceManager.isContentPlayLoop + SharedPreferenceManager.isContentPlayLoop = isPlayLoop + _isContentPlayLoopLiveData.value = isPlayLoop + } + + fun donation(contentId: Long, can: Int, comment: String, onSuccess: () -> Unit) { + isLoading.value = true + compositeDisposable.add( + repository.donation( + contentId = contentId, + can = can, + comment = comment, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + isLoading.value = false + + if (it.success) { + SharedPreferenceManager.can -= can + _toastLiveData.postValue( + "${can.moneyFormat()}캔을 후원하였습니다." + ) + onSuccess() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentReportDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentReportDialog.kt new file mode 100644 index 0000000..e39dc27 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentReportDialog.kt @@ -0,0 +1,60 @@ +package kr.co.vividnext.sodalive.audio_content.detail + +import android.app.Activity +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.WindowManager +import android.widget.RadioButton +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import kr.co.vividnext.sodalive.databinding.DialogAudioContentReportBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class AudioContentReportDialog( + activity: Activity, + layoutInflater: LayoutInflater, + confirmButtonClick: (String) -> Unit +) { + private val alertDialog: AlertDialog + val dialogView = DialogAudioContentReportBinding.inflate(layoutInflater) + var reason = "" + + init { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder.setView(dialogView.root) + + alertDialog = dialogBuilder.create() + alertDialog.setCancelable(false) + alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + dialogView.tvCancel.setOnClickListener { + alertDialog.dismiss() + } + + dialogView.tvReport.setOnClickListener { + if (reason.isNotBlank()) { + alertDialog.dismiss() + confirmButtonClick(reason) + } else { + Toast.makeText(activity, "신고 이유를 선택하세요.", Toast.LENGTH_LONG).show() + } + } + + dialogView.radioGroup.setOnCheckedChangeListener { radioGroup, checkedId -> + val radioButton = radioGroup.findViewById(checkedId) + reason = radioButton.text.toString() + } + } + + fun show(width: Int) { + alertDialog.show() + + val lp = WindowManager.LayoutParams() + lp.copyFrom(alertDialog.window?.attributes) + lp.width = width - (26.7f.dpToPx()).toInt() + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + alertDialog.window?.attributes = lp + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/GetAudioContentDetailResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/GetAudioContentDetailResponse.kt new file mode 100644 index 0000000..e32fb46 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/GetAudioContentDetailResponse.kt @@ -0,0 +1,45 @@ +package kr.co.vividnext.sodalive.audio_content.detail + +import com.google.gson.annotations.SerializedName +import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListItem +import kr.co.vividnext.sodalive.audio_content.order.OrderType + +data class GetAudioContentDetailResponse( + @SerializedName("contentId") val contentId: Long, + @SerializedName("title") val title: String, + @SerializedName("detail") val detail: String, + @SerializedName("coverImageUrl") val coverImageUrl: String, + @SerializedName("contentUrl") val contentUrl: String, + @SerializedName("themeStr") val themeStr: String, + @SerializedName("tag") val tag: String, + @SerializedName("price") val price: Int, + @SerializedName("duration") val duration: String, + @SerializedName("isAdult") val isAdult: Boolean, + @SerializedName("isMosaic") val isMosaic: Boolean, + @SerializedName("existOrdered") val existOrdered: Boolean, + @SerializedName("orderType") val orderType: OrderType?, + @SerializedName("remainingTime") val remainingTime: String?, + @SerializedName("creatorOtherContentList") + val creatorOtherContentList: List, + @SerializedName("sameThemeOtherContentList") + val sameThemeOtherContentList: List, + @SerializedName("isCommentAvailable") val isCommentAvailable: Boolean, + @SerializedName("isLike") val isLike: Boolean, + @SerializedName("likeCount") val likeCount: Int, + @SerializedName("commentList") val commentList: List, + @SerializedName("commentCount") val commentCount: Int, + @SerializedName("creator") val creator: AudioContentCreator +) + +data class OtherContentResponse( + @SerializedName("contentId") val contentId: Long, + @SerializedName("title") val title: String, + @SerializedName("coverUrl") val coverUrl: String, +) + +data class AudioContentCreator( + @SerializedName("creatorId") val creatorId: Long, + @SerializedName("nickname") val nickname: String, + @SerializedName("profileImageUrl") val profileImageUrl: String, + @SerializedName("isFollowing") val isFollowing: Boolean +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/OtherContentAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/OtherContentAdapter.kt new file mode 100644 index 0000000..a1a5976 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/OtherContentAdapter.kt @@ -0,0 +1,46 @@ +package kr.co.vividnext.sodalive.audio_content.detail + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemAudioOtherContentBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class OtherContentAdapter( + private val onClickItem: (Long) -> Unit +) : RecyclerView.Adapter() { + + val items = mutableListOf() + + inner class ViewHolder( + private val binding: ItemAudioOtherContentBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: OtherContentResponse) { + binding.ivCover.load(item.coverUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(2.7f.dpToPx())) + } + + binding.tvTitle.text = item.title + binding.root.setOnClickListener { onClickItem(item.contentId) } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemAudioOtherContentBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount() = items.count() +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/PutAudioContentLikeRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/PutAudioContentLikeRequest.kt new file mode 100644 index 0000000..9cfc49a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/PutAudioContentLikeRequest.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.audio_content.detail + +import com.google.gson.annotations.SerializedName + +data class PutAudioContentLikeRequest( + @SerializedName("contentId") val contentId: Long +) + +data class PutAudioContentLikeResponse( + @SerializedName("like") val like: Boolean +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/donation/AudioContentDonationRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/donation/AudioContentDonationRequest.kt new file mode 100644 index 0000000..b77c93c --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/donation/AudioContentDonationRequest.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.audio_content.donation + +import com.google.gson.annotations.SerializedName + +data class AudioContentDonationRequest( + @SerializedName("contentId") val contentId: Long, + @SerializedName("donationCan") val donationCan: Int, + @SerializedName("comment") val comment: String, + @SerializedName("container") val container: String = "aos" +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainBannerAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainBannerAdapter.kt new file mode 100644 index 0000000..ad71b86 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainBannerAdapter.kt @@ -0,0 +1,41 @@ +package kr.co.vividnext.sodalive.audio_content.main + +import android.widget.FrameLayout +import android.widget.ImageView +import coil.load +import coil.transform.RoundedCornersTransformation +import com.zhpan.bannerview.BaseBannerAdapter +import com.zhpan.bannerview.BaseViewHolder +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.extensions.dpToPx + +class AudioContentMainBannerAdapter( + private val itemWidth: Int, + private val itemHeight: Int, + private val onClick: (GetAudioContentBannerResponse) -> Unit +) : BaseBannerAdapter() { + override fun bindData( + holder: BaseViewHolder, + data: GetAudioContentBannerResponse, + position: Int, + pageSize: Int + ) { + val ivBanner = holder.findViewById(R.id.iv_recommend_live) + val layoutParams = ivBanner.layoutParams as FrameLayout.LayoutParams + + layoutParams.width = itemWidth + layoutParams.height = itemHeight + + ivBanner.load(data.thumbnailImageUrl) { + crossfade(true) + placeholder(R.drawable.ic_logo) + transformations(RoundedCornersTransformation(5.3f.dpToPx())) + } + ivBanner.layoutParams = layoutParams + ivBanner.setOnClickListener { onClick(data) } + } + + override fun getLayoutId(viewType: Int): Int { + return R.layout.item_recommend_live + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainContentAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainContentAdapter.kt new file mode 100644 index 0000000..4bc53d8 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainContentAdapter.kt @@ -0,0 +1,41 @@ +package kr.co.vividnext.sodalive.audio_content.main + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainBinding + +class AudioContentMainContentAdapter( + private val onClickItem: (Long) -> Unit, + private val onClickCreator: (Long) -> Unit, +) : RecyclerView.Adapter() { + + private val items = mutableListOf() + + 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) { + this.items.clear() + this.items.addAll(items) + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainCurationAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainCurationAdapter.kt new file mode 100644 index 0000000..34b3883 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainCurationAdapter.kt @@ -0,0 +1,94 @@ +package kr.co.vividnext.sodalive.audio_content.main + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Rect +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainCurationBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class AudioContentMainCurationAdapter( + private val onClickItem: (Long) -> Unit, + private val onClickCreator: (Long) -> Unit, +) : RecyclerView.Adapter() { + + private val items = mutableListOf() + + 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 + setAudioContentList(item.audioContents) + } + + private fun setAudioContentList(audioContents: List) { + 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) { + this.items.clear() + this.items.addAll(items) + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainFragment.kt new file mode 100644 index 0000000..63a9f9f --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainFragment.kt @@ -0,0 +1,470 @@ +package kr.co.vividnext.sodalive.audio_content.main + +import android.app.Service +import android.content.Intent +import android.graphics.Rect +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.LinearLayout +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.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.order.AudioContentOrderListActivity +import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity +import kr.co.vividnext.sodalive.base.BaseFragment +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainBinding +import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.settings.event.EventDetailActivity +import kr.co.vividnext.sodalive.settings.notification.MemberRole +import org.koin.android.ext.android.inject +import kotlin.math.roundToInt + +class AudioContentMainFragment : BaseFragment( + FragmentAudioContentMainBinding::inflate +) { + private val viewModel: AudioContentMainViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + private lateinit var imm: InputMethodManager + + private lateinit var newContentCreatorAdapter: AudioContentMainNewContentCreatorAdapter + private lateinit var bannerAdapter: AudioContentMainBannerAdapter + private lateinit var orderListAdapter: AudioContentMainContentAdapter + private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter + private lateinit var newContentAdapter: AudioContentMainContentAdapter + private lateinit var curationAdapter: AudioContentMainCurationAdapter + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + loadingDialog = LoadingDialog(requireActivity(), layoutInflater) + imm = requireContext().getSystemService( + Service.INPUT_METHOD_SERVICE + ) as InputMethodManager + + setupView() + bindData() + + viewModel.getMain() + } + + private fun setupView() { + if (SharedPreferenceManager.role == MemberRole.CREATOR.name) { + binding.llUploadContent.visibility = View.VISIBLE + binding.llUploadContent.setOnClickListener { + startActivity( + Intent( + requireActivity(), + AudioContentUploadActivity::class.java + ) + ) + } + } else { + binding.llUploadContent.visibility = View.GONE + } + + setupNewContentCreator() + setupBanner() + setupOrderList() + setupNewContentTheme() + setupNewContent() + setupCuration() + + binding.swipeRefreshLayout.setOnRefreshListener { + binding.swipeRefreshLayout.isRefreshing = false + viewModel.getMain() + } + } + + private fun setupNewContentCreator() { + newContentCreatorAdapter = AudioContentMainNewContentCreatorAdapter { + val intent = Intent(requireContext(), UserProfileActivity::class.java) + intent.putExtra(Constants.EXTRA_USER_ID, it) + startActivity(intent) + } + + binding.rvNewContentCreator.layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.HORIZONTAL, + false + ) + + binding.rvNewContentCreator.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.left = 0 + outRect.right = 10.7f.dpToPx().toInt() + } + + orderListAdapter.itemCount - 1 -> { + outRect.left = 10.7f.dpToPx().toInt() + outRect.right = 0 + } + + else -> { + outRect.left = 10.7f.dpToPx().toInt() + outRect.right = 10.7f.dpToPx().toInt() + } + } + } + }) + + binding.rvNewContentCreator.adapter = newContentCreatorAdapter + } + + private fun setupBanner() { + val layoutParams = binding + .rvBanner + .layoutParams as LinearLayout.LayoutParams + + val pagerWidth = screenWidth.toDouble() - 26.7f.dpToPx() + val pagerHeight = (pagerWidth * 0.53).roundToInt() + layoutParams.width = pagerWidth.roundToInt() + layoutParams.height = pagerHeight + + bannerAdapter = AudioContentMainBannerAdapter(pagerWidth.roundToInt(), pagerHeight) { + when (it.type) { + AudioContentBannerType.EVENT -> { + startActivity( + Intent(requireContext(), EventDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_EVENT, it.eventItem!!) + } + ) + } + + AudioContentBannerType.CREATOR -> { + startActivity( + Intent(requireContext(), UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, it.creatorId!!) + } + ) + } + + AudioContentBannerType.LINK -> { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!))) + } + } + } + + binding + .rvBanner + .layoutParams = layoutParams + + binding.rvBanner.apply { + adapter = bannerAdapter as BaseBannerAdapter + + setLifecycleRegistry(lifecycle) + setScrollDuration(1000) + setInterval(4 * 1000) + }.create() + + binding + .rvBanner + .setIndicatorView(binding.indicatorBanner) + .setIndicatorStyle(IndicatorStyle.ROUND_RECT) + .setIndicatorSlideMode(IndicatorSlideMode.SMOOTH) + .setIndicatorVisibility(View.GONE) + .setIndicatorSliderColor( + ContextCompat.getColor(requireContext(), R.color.color_909090), + ContextCompat.getColor(requireContext(), R.color.color_9970ff) + ) + .setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt()) + .setIndicatorHeight(4f.dpToPx().toInt()) + } + + private fun setupOrderList() { + orderListAdapter = AudioContentMainContentAdapter( + onClickItem = { + startActivity( + Intent(requireContext(), AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) + } + ) + }, + onClickCreator = { + startActivity( + Intent(requireContext(), UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, it) + } + ) + } + ) + + binding.rvMyStash.layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.HORIZONTAL, + false + ) + + binding.rvMyStash.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.left = 0 + outRect.right = 6.7f.dpToPx().toInt() + } + + orderListAdapter.itemCount - 1 -> { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 0 + } + + else -> { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 6.7f.dpToPx().toInt() + } + } + } + }) + + binding.rvMyStash.adapter = orderListAdapter + binding.tvMyStashViewAll.setOnClickListener { + startActivity(Intent(requireContext(), AudioContentOrderListActivity::class.java)) + } + } + + private fun setupNewContentTheme() { + newContentThemeAdapter = AudioContentMainNewContentThemeAdapter { + viewModel.getNewContentOfTheme(theme = it) + } + + binding.rvNewContentTheme.layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.HORIZONTAL, + false + ) + + binding.rvNewContentTheme.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.left = 0 + outRect.right = 4f.dpToPx().toInt() + } + + orderListAdapter.itemCount - 1 -> { + outRect.left = 4f.dpToPx().toInt() + outRect.right = 0 + } + + else -> { + outRect.left = 4f.dpToPx().toInt() + outRect.right = 4f.dpToPx().toInt() + } + } + } + }) + + binding.rvNewContentTheme.adapter = newContentThemeAdapter + } + + private fun setupNewContent() { + newContentAdapter = 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() + } + + orderListAdapter.itemCount - 1 -> { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 0 + } + + else -> { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 6.7f.dpToPx().toInt() + } + } + } + }) + + binding.rvNewContent.adapter = newContentAdapter + } + + 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) + } + ) + } + ) + + binding.rvCuration.layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.VERTICAL, + false + ) + + binding.rvCuration.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.top = 40f.dpToPx().toInt() + outRect.bottom = 20f.dpToPx().toInt() + } + + curationAdapter.itemCount - 1 -> { + outRect.top = 20f.dpToPx().toInt() + outRect.bottom = 40f.dpToPx().toInt() + } + + else -> { + outRect.top = 20f.dpToPx().toInt() + outRect.bottom = 20f.dpToPx().toInt() + } + } + } + }) + binding.rvCuration.adapter = curationAdapter + } + + private fun bindData() { + viewModel.isLoading.observe(viewLifecycleOwner) { + if (it) { + loadingDialog.show(screenWidth) + } else { + loadingDialog.dismiss() + } + } + + viewModel.toastLiveData.observe(viewLifecycleOwner) { + it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() } + } + + viewModel.newContentUploadCreatorListLiveData.observe(viewLifecycleOwner) { + newContentCreatorAdapter.addItems(it) + binding.rvNewContentCreator.visibility = if ( + newContentCreatorAdapter.itemCount <= 0 && it.isEmpty() + ) { + View.GONE + } else { + View.VISIBLE + } + } + + viewModel.bannerLiveData.observe(viewLifecycleOwner) { + if (bannerAdapter.itemCount <= 0 && it.isEmpty()) { + binding.rvBanner.visibility = View.GONE + binding.indicatorBanner.visibility = View.GONE + } else { + binding.rvBanner.visibility = View.VISIBLE + binding.indicatorBanner.visibility = View.VISIBLE + binding.rvBanner.refreshData(it) + } + } + + viewModel.orderListLiveData.observe(viewLifecycleOwner) { + orderListAdapter.addItems(it) + binding.llMyStash.visibility = if ( + orderListAdapter.itemCount <= 0 && it.isEmpty() + ) { + View.GONE + } else { + View.VISIBLE + } + } + + viewModel.newContentListLiveData.observe(viewLifecycleOwner) { + newContentAdapter.addItems(it) + } + + viewModel.themeListLiveData.observe(viewLifecycleOwner) { + binding.llNewContent.visibility = View.VISIBLE + newContentThemeAdapter.addItems(it) + } + + viewModel.curationListLiveData.observe(viewLifecycleOwner) { + curationAdapter.addItems(it) + binding.rvCuration.visibility = if ( + curationAdapter.itemCount <= 0 && it.isEmpty() + ) { + View.GONE + } else { + View.VISIBLE + } + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainItemViewHolder.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainItemViewHolder.kt new file mode 100644 index 0000000..64d3081 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainItemViewHolder.kt @@ -0,0 +1,41 @@ +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.ivAudioContentCoverImage.load(item.coverImageUrl) { + crossfade(true) + placeholder(R.drawable.ic_logo) + transformations(RoundedCornersTransformation(2.7f.dpToPx())) + } + + binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) { + crossfade(true) + placeholder(R.drawable.ic_logo) + transformations(CircleCropTransformation()) + } + + binding.tvAudioContentTitle.text = item.title + binding.tvAudioContentCreatorNickname.text = item.creatorNickname + + binding.ivAudioContentCreator.setOnClickListener { onClickCreator(item.creatorId) } + binding.iv19.visibility = if (item.isAdult) { + View.VISIBLE + } else { + View.GONE + } + binding.root.setOnClickListener { onClickItem(item.contentId) } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainNewContentCreatorAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainNewContentCreatorAdapter.kt new file mode 100644 index 0000000..fe778b7 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainNewContentCreatorAdapter.kt @@ -0,0 +1,52 @@ +package kr.co.vividnext.sodalive.audio_content.main + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainNewContentCreatorBinding + +class AudioContentMainNewContentCreatorAdapter( + private val onClickItem: (Long) -> Unit +) : RecyclerView.Adapter() { + + private val items = mutableListOf() + + inner class ViewHolder( + private val binding: ItemAudioContentMainNewContentCreatorBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: GetNewContentUploadCreator) { + binding.tvNewContentCreator.text = item.creatorNickname + binding.ivNewContentCreator.load(item.creatorProfileImageUrl) { + crossfade(true) + placeholder(R.drawable.ic_logo) + transformations(CircleCropTransformation()) + } + binding.root.setOnClickListener { onClickItem(item.creatorId) } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemAudioContentMainNewContentCreatorBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun getItemCount() = items.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + @SuppressLint("NotifyDataSetChanged") + fun addItems(items: List) { + this.items.clear() + this.items.addAll(items) + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainNewContentThemeAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainNewContentThemeAdapter.kt new file mode 100644 index 0000000..dac9789 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainNewContentThemeAdapter.kt @@ -0,0 +1,68 @@ +package kr.co.vividnext.sodalive.audio_content.main + +import android.annotation.SuppressLint +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainNewContentThemeBinding + +class AudioContentMainNewContentThemeAdapter( + private val onClickItem: (String) -> Unit +) : RecyclerView.Adapter() { + + private val themeList = mutableListOf() + private var selectedTheme = "" + + inner class ViewHolder( + private val context: Context, + private val binding: ItemAudioContentMainNewContentThemeBinding + ) : RecyclerView.ViewHolder(binding.root) { + @SuppressLint("NotifyDataSetChanged") + fun bind(theme: String) { + if (theme == selectedTheme || (selectedTheme == "" && theme == "전체")) { + binding.tvTheme.setBackgroundResource( + R.drawable.bg_round_corner_16_7_transparent_9970ff + ) + binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_9970ff)) + } else { + binding.tvTheme.setBackgroundResource( + R.drawable.bg_round_corner_16_7_transparent_777777 + ) + binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_777777)) + } + + binding.tvTheme.text = theme + binding.root.setOnClickListener { + onClickItem(theme) + selectedTheme = theme + notifyDataSetChanged() + } + } + } + + @SuppressLint("NotifyDataSetChanged") + fun addItems(themeList: List) { + this.selectedTheme = "" + this.themeList.clear() + this.themeList.addAll(themeList) + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + parent.context, + ItemAudioContentMainNewContentThemeBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun getItemCount() = themeList.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(themeList[position]) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainViewModel.kt new file mode 100644 index 0000000..922d42f --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainViewModel.kt @@ -0,0 +1,121 @@ +package kr.co.vividnext.sodalive.audio_content.main + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.audio_content.AudioContentRepository +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager + +class AudioContentMainViewModel( + private val repository: AudioContentRepository +) : BaseViewModel() { + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private var _newContentUploadCreatorListLiveData = + MutableLiveData>() + val newContentUploadCreatorListLiveData: LiveData> + get() = _newContentUploadCreatorListLiveData + + private var _newContentListLiveData = MutableLiveData>() + val newContentListLiveData: LiveData> + get() = _newContentListLiveData + + private var _bannerLiveData = MutableLiveData>() + val bannerLiveData: LiveData> + get() = _bannerLiveData + + private var _orderListLiveData = MutableLiveData>() + val orderListLiveData: LiveData> + get() = _orderListLiveData + + private var _themeListLiveData = MutableLiveData>() + val themeListLiveData: LiveData> + get() = _themeListLiveData + + private var _curationListLiveData = MutableLiveData>() + val curationListLiveData: LiveData> + get() = _curationListLiveData + + fun getMain() { + _isLoading.value = true + compositeDisposable.add( + repository.getMain(token = "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + val data = it.data + _newContentUploadCreatorListLiveData.value = + data.newContentUploadCreatorList + _newContentListLiveData.value = data.newContentList + _orderListLiveData.value = data.orderList + _bannerLiveData.value = data.bannerList + _curationListLiveData.value = data.curationList + + val themeList = listOf("전체").union(data.themeList).toList() + _themeListLiveData.value = themeList + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun getNewContentOfTheme(theme: String) { + compositeDisposable.add( + repository.getNewContentOfTheme( + theme = if (theme == "전체") { + "" + } else { + theme + }, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + _newContentListLiveData.value = it.data!! + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/GetAudioContentMainResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/GetAudioContentMainResponse.kt new file mode 100644 index 0000000..e05c47f --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/GetAudioContentMainResponse.kt @@ -0,0 +1,55 @@ +package kr.co.vividnext.sodalive.audio_content.main + +import com.google.gson.annotations.SerializedName +import kr.co.vividnext.sodalive.settings.event.EventItem + +data class GetAudioContentMainResponse( + @SerializedName("newContentUploadCreatorList") + val newContentUploadCreatorList: List, + @SerializedName("bannerList") val bannerList: List, + @SerializedName("orderList") val orderList: List, + @SerializedName("themeList") val themeList: List, + @SerializedName("newContentList") val newContentList: List, + @SerializedName("curationList") val curationList: List +) + +data class GetNewContentUploadCreator( + @SerializedName("creatorId") val creatorId: Long, + @SerializedName("creatorNickname") val creatorNickname: String, + @SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String +) + +data class GetAudioContentMainItem( + @SerializedName("contentId") val contentId: Long, + @SerializedName("coverImageUrl") val coverImageUrl: String, + @SerializedName("title") val title: String, + @SerializedName("isAdult") val isAdult: Boolean, + @SerializedName("creatorId") val creatorId: Long, + @SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String, + @SerializedName("creatorNickname") val creatorNickname: String +) + +data class GetAudioContentCurationResponse( + @SerializedName("title") val title: String, + @SerializedName("description") val description: String, + @SerializedName("audioContents") val audioContents: List +) + +data class GetAudioContentBannerResponse( + @SerializedName("type") val type: AudioContentBannerType, + @SerializedName("thumbnailImageUrl") val thumbnailImageUrl: String, + @SerializedName("eventItem") val eventItem: EventItem?, + @SerializedName("creatorId") val creatorId: Long?, + @SerializedName("link") val link: String? +) + +enum class AudioContentBannerType { + @SerializedName("EVENT") + EVENT, + + @SerializedName("CREATOR") + CREATOR, + + @SerializedName("LINK") + LINK +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/AudioContentModifyActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/AudioContentModifyActivity.kt new file mode 100644 index 0000000..7730ef7 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/AudioContentModifyActivity.kt @@ -0,0 +1,288 @@ +package kr.co.vividnext.sodalive.audio_content.modify + +import android.Manifest +import android.annotation.SuppressLint +import android.os.Build +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import androidx.core.view.setPadding +import coil.load +import coil.transform.RoundedCornersTransformation +import com.github.dhaval2404.imagepicker.ImagePicker +import com.gun0912.tedpermission.PermissionListener +import com.gun0912.tedpermission.normal.TedPermission +import com.jakewharton.rxbinding4.widget.textChanges +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.RealPathUtil +import kr.co.vividnext.sodalive.databinding.ActivityAudioContentModifyBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject + +class AudioContentModifyActivity : BaseActivity( + ActivityAudioContentModifyBinding::inflate +) { + + private val viewModel: AudioContentModifyViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + + private val imageResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + val resultCode = result.resultCode + val data = result.data + + if (resultCode == RESULT_OK) { + val fileUri = data?.data + + if (fileUri != null) { + binding.ivCover.setPadding(0) + binding.ivCover.background = null + binding.ivCover.load(fileUri) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(13.3f.dpToPx())) + } + viewModel.coverImageUri = fileUri + } else { + Toast.makeText( + this, + "잘못된 파일입니다.\n다시 선택해 주세요.", + Toast.LENGTH_SHORT + ).show() + } + } else if (resultCode == ImagePicker.RESULT_ERROR) { + Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0) + if (audioContentId <= 0) { + Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show() + finish() + } + + checkPermissions() + + viewModel.getRealPathFromURI = { + RealPathUtil.getRealPath(applicationContext, it) + } + + bindData() + viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() } + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + binding.toolbar.tvBack.text = "콘텐츠 수정" + binding.toolbar.tvBack.setOnClickListener { finish() } + + binding.ivPhotoPicker.setOnClickListener { + ImagePicker.with(this) + .crop() + .galleryOnly() + .galleryMimeTypes( // Exclude gif images + mimeTypes = arrayOf( + "image/png", + "image/jpg", + "image/jpeg" + ) + ) + .createIntent { imageResult.launch(it) } + } + + binding.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) } + binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) } + + binding.tvCancel.setOnClickListener { finish() } + binding.tvModify.setOnClickListener { + viewModel.modifyAudioContent { finish() } + } + } + + private fun checkPermissions() { + val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + listOf(Manifest.permission.READ_MEDIA_AUDIO, Manifest.permission.READ_MEDIA_IMAGES) + } else { + listOf(Manifest.permission.READ_EXTERNAL_STORAGE) + } + + TedPermission.create() + .setPermissionListener(object : PermissionListener { + override fun onPermissionGranted() { + } + + override fun onPermissionDenied(deniedPermissions: MutableList?) { + finish() + } + }) + .setDeniedMessage(R.string.read_storage_permission_denied_message) + .setPermissions(*permissions.toTypedArray()) + .check() + } + + @SuppressLint("SetTextI18n") + private fun bindData() { + compositeDisposable.add( + binding.etTitle.textChanges().skip(1) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + viewModel.title = it.toString() + } + ) + + compositeDisposable.add( + binding.etDetail.textChanges().skip(1) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + binding.tvNumberOfCharacters.text = "${it.length}자" + viewModel.detail = it.toString() + } + ) + + viewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + viewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth, "") + } else { + loadingDialog.dismiss() + } + } + + viewModel.isAvailableCommentLiveData.observe(this) { + if (it) { + binding.ivCommentYes.visibility = View.VISIBLE + binding.tvCommentYes.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_eeeeee + ) + ) + binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff) + + binding.ivCommentNo.visibility = View.GONE + binding.tvCommentNo.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_9970ff + ) + ) + binding.llCommentNo.setBackgroundResource( + R.drawable.bg_round_corner_6_7_1f1734_9970ff + ) + } else { + binding.ivCommentNo.visibility = View.VISIBLE + binding.tvCommentNo.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_eeeeee + ) + ) + binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff) + + binding.ivCommentYes.visibility = View.GONE + binding.tvCommentYes.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_9970ff + ) + ) + binding.llCommentYes + .setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734_9970ff) + } + } + + viewModel.isAdultShowUiLiveData.observe(this) { + if (it) { + binding.llSetAdult.visibility = View.VISIBLE + + binding.llAgeAll.setOnClickListener { + viewModel.setAdult(false) + } + + binding.llAge19.setOnClickListener { + viewModel.setAdult(true) + } + + viewModel.isAdultLiveData.observe(this) { + if (it) { + binding.ivAgeAll.visibility = View.GONE + binding.llAgeAll.setBackgroundResource( + R.drawable.bg_round_corner_6_7_1f1734 + ) + binding.tvAgeAll.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_9970ff + ) + ) + + binding.ivAge19.visibility = View.VISIBLE + binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff) + binding.tvAge19.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_eeeeee + ) + ) + } else { + binding.ivAge19.visibility = View.GONE + binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734) + binding.tvAge19.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_9970ff + ) + ) + + binding.ivAgeAll.visibility = View.VISIBLE + binding.llAgeAll.setBackgroundResource( + R.drawable.bg_round_corner_6_7_9970ff + ) + binding.tvAgeAll.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_eeeeee + ) + ) + } + } + } else { + binding.llSetAdult.visibility = View.GONE + } + } + + viewModel.coverImageLiveData.observe(this) { + binding.ivCover.setPadding(0) + binding.ivCover.background = null + binding.ivCover.load(it) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(13.3f.dpToPx())) + } + } + + viewModel.titleLiveData.observe(this) { + binding.etTitle.setText(it) + } + + viewModel.detailLiveData.observe(this) { + binding.etDetail.setText(it) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/AudioContentModifyViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/AudioContentModifyViewModel.kt new file mode 100644 index 0000000..7700c85 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/AudioContentModifyViewModel.kt @@ -0,0 +1,212 @@ +package kr.co.vividnext.sodalive.audio_content.modify + +import android.net.Uri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.google.gson.Gson +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.audio_content.AudioContentRepository +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okio.BufferedSink +import java.io.File + +class AudioContentModifyViewModel( + private val repository: AudioContentRepository +) : BaseViewModel() { + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private val _isAdultLiveData = MutableLiveData(false) + val isAdultLiveData: LiveData + get() = _isAdultLiveData + + private val _isAvailableCommentLiveData = MutableLiveData(false) + val isAvailableCommentLiveData: LiveData + get() = _isAvailableCommentLiveData + + private val _titleLiveData = MutableLiveData("") + val titleLiveData: LiveData + get() = _titleLiveData + + private val _detailLiveData = MutableLiveData("") + val detailLiveData: LiveData + get() = _detailLiveData + + private val _coverImageLiveData = MutableLiveData("") + val coverImageLiveData: LiveData + get() = _coverImageLiveData + + private val _isAdultShowUiLiveData = MutableLiveData(true) + val isAdultShowUiLiveData: LiveData + get() = _isAdultShowUiLiveData + + lateinit var getRealPathFromURI: (Uri) -> String? + + var contentId: Long = 0 + var title: String? = null + var detail: String? = null + var coverImageUri: Uri? = null + + fun setAdult(isAdult: Boolean) { + _isAdultLiveData.postValue(isAdult) + } + + fun setAvailableComment(isAvailableComment: Boolean) { + _isAvailableCommentLiveData.postValue(isAvailableComment) + } + + fun getAudioContentDetail(audioContentId: Long, onFailure: (() -> Unit)? = null) { + this.contentId = audioContentId + _isLoading.value = true + + compositeDisposable.add( + repository.getAudioContentDetail( + audioContentId = audioContentId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + _titleLiveData.value = it.data.title + _detailLiveData.value = it.data.detail + _coverImageLiveData.value = it.data.coverImageUrl + _isAvailableCommentLiveData.value = it.data.isCommentAvailable + _isAdultLiveData.value = it.data.isAdult + _isAdultShowUiLiveData.value = !it.data.isAdult + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + + if (onFailure != null) { + onFailure() + } + } + + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + if (onFailure != null) { + onFailure() + } + } + ) + ) + } + + fun modifyAudioContent(onSuccess: () -> Unit) { + if (!_isLoading.value!! && contentId > 0 && validateData()) { + _isLoading.value = true + + val request = ModifyAudioContentRequest( + contentId = contentId, + title = title, + detail = detail, + isAdult = _isAdultLiveData.value!!, + isCommentAvailable = _isAvailableCommentLiveData.value!! + ) + + val requestJson = Gson().toJson(request) + + val coverImage = if (coverImageUri != null) { + val file = File(getRealPathFromURI(coverImageUri!!)) + MultipartBody.Part.createFormData( + "coverImage", + file.name, + body = object : RequestBody() { + override fun contentType(): MediaType { + return "image/*".toMediaType() + } + + override fun writeTo(sink: BufferedSink) { + file.inputStream().use { inputStream -> + val buffer = ByteArray(1024) + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + sink.write(buffer, 0, bytesRead) + } + } + } + + override fun contentLength(): Long { + return file.length() + } + } + ) + } else { + null + } + + compositeDisposable.add( + repository.modifyAudioContent( + coverImage = coverImage, + request = requestJson.toRequestBody("text/plain".toMediaType()), + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success) { + _toastLiveData.postValue("수정되었습니다.") + onSuccess() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + _isLoading.postValue(false) + }, + { + _isLoading.postValue(false) + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + ) + ) + } + } + + private fun validateData(): Boolean { + if (title != null && title!!.isBlank()) { + _toastLiveData.postValue("제목을 입력해 주세요.") + return false + } + + if (detail != null && (detail!!.isBlank() || detail!!.length < 5)) { + _toastLiveData.postValue("내용을 5자 이상 입력해 주세요.") + return false + } + + return true + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/ModifyAudioContentRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/ModifyAudioContentRequest.kt new file mode 100644 index 0000000..53cdb57 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/modify/ModifyAudioContentRequest.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.audio_content.modify + +import com.google.gson.annotations.SerializedName + +data class ModifyAudioContentRequest( + @SerializedName("contentId") val contentId: Long, + @SerializedName("title") val title: String?, + @SerializedName("detail") val detail: String?, + @SerializedName("isAdult") val isAdult: Boolean, + @SerializedName("isCommentAvailable") val isCommentAvailable: Boolean +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderConfirmDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderConfirmDialog.kt new file mode 100644 index 0000000..6ae8686 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderConfirmDialog.kt @@ -0,0 +1,100 @@ +package kr.co.vividnext.sodalive.audio_content.order + +import android.app.Activity +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import coil.load +import coil.transform.CircleCropTransformation +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.DialogAudioContentOrderConfirmBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kotlin.math.ceil + +class AudioContentOrderConfirmDialog( + activity: Activity, + layoutInflater: LayoutInflater, + title: String, + theme: String, + coverImageUrl: String, + isAdult: Boolean, + profileImageUrl: String, + nickname: String, + duration: String, + orderType: OrderType, + price: Int, + confirmButtonClick: () -> Unit, +) { + + private val alertDialog: AlertDialog + + val dialogView = DialogAudioContentOrderConfirmBinding.inflate(layoutInflater) + + init { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder.setView(dialogView.root) + + alertDialog = dialogBuilder.create() + alertDialog.setCancelable(false) + alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + dialogView.tvTitle.text = title + dialogView.tvTheme.text = theme + dialogView.tvProfileNickname.text = nickname + + dialogView.ivCover.load(coverImageUrl) { + crossfade(true) + placeholder(R.drawable.ic_logo) + transformations(RoundedCornersTransformation(4f)) + } + + dialogView.ivProfile.load(profileImageUrl) { + crossfade(true) + placeholder(R.drawable.ic_logo) + transformations(CircleCropTransformation()) + } + + dialogView.tvDuration.text = duration + dialogView.tvPrice.text = if (orderType == OrderType.RENTAL) { + "${ceil(price * 0.7).toInt()}" + } else { + "$price" + } + + dialogView.iv19.visibility = if (isAdult) { + View.VISIBLE + } else { + View.GONE + } + + dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) { + "콘텐츠를 대여하시겠습니까?\n아래 코인이 차감됩니다." + } else { + "콘텐츠를 소장하시겠습니까?\n아래 코인이 차감됩니다." + } + + dialogView.tvCancel.setOnClickListener { + alertDialog.dismiss() + } + + dialogView.tvConfirm.setOnClickListener { + alertDialog.dismiss() + confirmButtonClick() + } + } + + fun show(width: Int) { + alertDialog.show() + + val lp = WindowManager.LayoutParams() + lp.copyFrom(alertDialog.window?.attributes) + lp.width = width - (26.7f.dpToPx()).toInt() + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + alertDialog.window?.attributes = lp + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderFragment.kt new file mode 100644 index 0000000..d061de5 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderFragment.kt @@ -0,0 +1,44 @@ +package kr.co.vividnext.sodalive.audio_content.order + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kr.co.vividnext.sodalive.databinding.FragmentAudioContentOrderBinding +import kotlin.math.ceil + +class AudioContentOrderFragment( + private val price: Int, + private val onClickRental: () -> Unit, + private val onClickKeep: () -> Unit +) : BottomSheetDialogFragment() { + + private lateinit var binding: FragmentAudioContentOrderBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentAudioContentOrderBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.tvKeep.text = "$price" + binding.tvRental.text = "${ceil(price * 0.7).toInt()}" + + binding.llKeep.setOnClickListener { + onClickKeep() + dismiss() + } + + binding.llRental.setOnClickListener { + onClickRental() + dismiss() + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListActivity.kt new file mode 100644 index 0000000..75872ed --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListActivity.kt @@ -0,0 +1,110 @@ +package kr.co.vividnext.sodalive.audio_content.order + +import android.annotation.SuppressLint +import android.content.Intent +import android.graphics.Rect +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.databinding.ActivityAudioContentOrderListBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject + +class AudioContentOrderListActivity : BaseActivity( + ActivityAudioContentOrderListBinding::inflate +) { + + private val viewModel: AudioContentOrderListViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + private lateinit var adapter: AudioContentOrderListAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + bindData() + viewModel.getAudioContentOrderList { finish() } + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + binding.toolbar.tvBack.text = "구매목록" + binding.toolbar.tvBack.setOnClickListener { finish() } + + adapter = AudioContentOrderListAdapter { + startActivity( + Intent(applicationContext, AudioContentDetailActivity::class.java) + .apply { putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) } + ) + } + + binding.rvOrderList.layoutManager = LinearLayoutManager( + applicationContext, + LinearLayoutManager.VERTICAL, + false + ) + + binding.rvOrderList.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + outRect.left = 13.3f.dpToPx().toInt() + outRect.right = 13.3f.dpToPx().toInt() + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.top = 13.3f.dpToPx().toInt() + outRect.bottom = 6.7f.dpToPx().toInt() + } + + adapter.itemCount - 1 -> { + outRect.top = 6.7f.dpToPx().toInt() + outRect.bottom = 13.3f.dpToPx().toInt() + } + + else -> { + outRect.top = 6.7f.dpToPx().toInt() + outRect.bottom = 6.7f.dpToPx().toInt() + } + } + } + }) + + binding.rvOrderList.adapter = adapter + } + + @SuppressLint("NotifyDataSetChanged") + private fun bindData() { + viewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + viewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth, "") + } else { + loadingDialog.dismiss() + } + } + + viewModel.orderList.observe(this) { + if (viewModel.page == 2) { + adapter.items.clear() + } + + adapter.items.addAll(it) + adapter.notifyDataSetChanged() + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListAdapter.kt new file mode 100644 index 0000000..89dbe5c --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListAdapter.kt @@ -0,0 +1,61 @@ +package kr.co.vividnext.sodalive.audio_content.order + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemAudioContentOrderListBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.extensions.moneyFormat + +class AudioContentOrderListAdapter( + private val onItemClick: (Long) -> Unit +) : RecyclerView.Adapter() { + + var items = mutableSetOf() + + inner class ViewHolder( + private val binding: ItemAudioContentOrderListBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: GetAudioContentOrderListItem) { + binding.ivCover.load(item.coverImageUrl) { + crossfade(true) + placeholder(R.drawable.ic_logo) + transformations(RoundedCornersTransformation(5.3f.dpToPx())) + } + + binding.tvTitle.text = item.title + binding.tvTheme.text = item.themeStr + binding.tvDuration.text = item.duration + binding.tvLikeCount.text = item.likeCount.moneyFormat() + binding.tvCommentCount.text = item.commentCount.moneyFormat() + + if (item.orderType == OrderType.RENTAL) { + binding.tvRental.visibility = View.VISIBLE + binding.tvPurchased.visibility = View.GONE + } else { + binding.tvPurchased.visibility = View.VISIBLE + binding.tvRental.visibility = View.GONE + } + + binding.root.setOnClickListener { onItemClick(item.contentId) } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemAudioContentOrderListBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items.toList()[position]) + } + + override fun getItemCount() = items.size +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListViewModel.kt new file mode 100644 index 0000000..6f15287 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/AudioContentOrderListViewModel.kt @@ -0,0 +1,77 @@ +package kr.co.vividnext.sodalive.audio_content.order + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.audio_content.AudioContentRepository +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager + +class AudioContentOrderListViewModel( + private val repository: AudioContentRepository +) : BaseViewModel() { + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private var _orderList = MutableLiveData>() + val orderList: LiveData> + get() = _orderList + + private var isLast = false + var page = 1 + private val size = 10 + + fun getAudioContentOrderList(onFailure: (() -> Unit)? = null) { + _isLoading.value = true + compositeDisposable.add( + repository.getAudioContentOrderList( + page = page, + size = size, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + if (it.data.items.isNotEmpty()) { + page += 1 + _orderList.postValue(it.data.items) + } else { + isLast = true + } + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + + if (onFailure != null) { + onFailure() + } + } + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + if (onFailure != null) { + onFailure() + } + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/GetAudioContentOrderListResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/GetAudioContentOrderListResponse.kt new file mode 100644 index 0000000..315da8d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/GetAudioContentOrderListResponse.kt @@ -0,0 +1,20 @@ +package kr.co.vividnext.sodalive.audio_content.order + +import com.google.gson.annotations.SerializedName + +data class GetAudioContentOrderListResponse( + @SerializedName("totalCount") val totalCount: Int, + @SerializedName("items") val items: List +) + +data class GetAudioContentOrderListItem( + @SerializedName("contentId") val contentId: Long, + @SerializedName("coverImageUrl") val coverImageUrl: String, + @SerializedName("title") val title: String, + @SerializedName("themeStr") val themeStr: String, + @SerializedName("duration") val duration: String?, + @SerializedName("isAdult") val isAdult: Boolean, + @SerializedName("orderType") val orderType: OrderType, + @SerializedName("likeCount") val likeCount: Int, + @SerializedName("commentCount") val commentCount: Int, +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/OrderRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/OrderRequest.kt new file mode 100644 index 0000000..f9dc234 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/OrderRequest.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.audio_content.order + +import com.google.gson.annotations.SerializedName + +data class OrderRequest( + @SerializedName("contentId") val contentId: Long, + @SerializedName("orderType") val orderType: OrderType, + @SerializedName("container") val container: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/OrderType.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/OrderType.kt new file mode 100644 index 0000000..9321413 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/order/OrderType.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.audio_content.order + +import com.google.gson.annotations.SerializedName + +enum class OrderType { + @SerializedName("RENTAL") + RENTAL, + + @SerializedName("KEEP") + KEEP +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/AudioContentUploadActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/AudioContentUploadActivity.kt new file mode 100644 index 0000000..87adcb8 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/AudioContentUploadActivity.kt @@ -0,0 +1,435 @@ +package kr.co.vividnext.sodalive.audio_content.upload + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.OpenableColumns +import android.view.View +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import coil.load +import coil.transform.CircleCropTransformation +import coil.transform.RoundedCornersTransformation +import com.github.dhaval2404.imagepicker.ImagePicker +import com.gun0912.tedpermission.PermissionListener +import com.gun0912.tedpermission.normal.TedPermission +import com.jakewharton.rxbinding4.widget.textChanges +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeFragment +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.RealPathUtil +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.databinding.ActivityAudioContentUploadBinding +import kr.co.vividnext.sodalive.dialog.LiveDialog +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject +import java.io.File + +class AudioContentUploadActivity : BaseActivity( + ActivityAudioContentUploadBinding::inflate +) { + + private val viewModel: AudioContentUploadViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + + private val themeFragment: AudioContentThemeFragment by lazy { + AudioContentThemeFragment( + getSelectedTheme = { viewModel.theme }, + onItemClick = { + binding.ivTheme.load(it.image) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + + binding.ivTheme.visibility = View.VISIBLE + } + binding.tvTheme.text = it.theme + viewModel.theme = it + } + ) + } + + private val imageResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + val resultCode = result.resultCode + val data = result.data + + if (resultCode == RESULT_OK) { + val fileUri = data?.data + + if (fileUri != null) { + binding.ivCover.background = null + binding.ivCover.load(fileUri) { + crossfade(true) + placeholder(R.drawable.ic_logo) + transformations(RoundedCornersTransformation(13.3f.dpToPx())) + } + viewModel.coverImageUri = fileUri + } else { + Toast.makeText( + this, + "잘못된 파일입니다.\n다시 선택해 주세요.", + Toast.LENGTH_SHORT + ).show() + } + } else if (resultCode == ImagePicker.RESULT_ERROR) { + Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show() + } + } + + private val selectAudioActivityResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + val resultCode = result.resultCode + val data = result.data + + if (resultCode == RESULT_OK) { + val fileUri = data?.data + if (fileUri != null) { + binding.tvSelectContent.text = getFileName(fileUri) + viewModel.contentUri = fileUri + } else { + Toast.makeText( + this, + "잘못된 파일입니다.\n다시 선택해 주세요.", + Toast.LENGTH_SHORT + ).show() + } + } else if (resultCode == ImagePicker.RESULT_ERROR) { + binding.tvSelectContent.text = "파일 선택" + viewModel.contentUri = null + Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + checkPermissions() + + viewModel.getRealPathFromURI = { + RealPathUtil.getRealPath(applicationContext, it) + } + + bindData() + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + + binding.toolbar.tvBack.text = "콘텐츠 등록" + binding.toolbar.tvBack.setOnClickListener { finish() } + binding.llTheme.setOnClickListener { + if (themeFragment.isAdded) return@setOnClickListener + + themeFragment.show(supportFragmentManager, themeFragment.tag) + } + + binding.ivPhotoPicker.setOnClickListener { + ImagePicker.with(this) + .crop() + .galleryOnly() + .galleryMimeTypes( // Exclude gif images + mimeTypes = arrayOf( + "image/png", + "image/jpg", + "image/jpeg" + ) + ) + .createIntent { imageResult.launch(it) } + } + + binding.tvSelectContent.setOnClickListener { + val intent = Intent().apply { + type = "audio/*" + action = Intent.ACTION_GET_CONTENT + addCategory(Intent.CATEGORY_OPENABLE) + } + selectAudioActivityResultLauncher.launch( + Intent.createChooser( + intent, + "Select Audio" + ) + ) + } + + if (SharedPreferenceManager.isAuth) { + binding.llSetAdult.visibility = View.VISIBLE + } else { + binding.llSetAdult.visibility = View.GONE + } + + binding.llPricePaid.setOnClickListener { viewModel.setPriceFree(false) } + binding.llPriceFree.setOnClickListener { viewModel.setPriceFree(true) } + binding.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) } + binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) } + + binding.tvCancel.setOnClickListener { finish() } + binding.tvUpload.setOnClickListener { + viewModel.uploadAudioContent { + LiveDialog( + activity = this, + layoutInflater = layoutInflater, + title = "콘텐츠 업로드", + desc = "등록한 콘텐츠가 업로드 중입니다.\n" + + "콘텐츠 등록이 완료되면 알림을 보내드립니다.\n" + + "이 페이지를 나가도 콘텐츠는 자동으로 등록됩니다.", + confirmButtonTitle = "확인", + confirmButtonClick = { finish() }, + ).show(screenWidth) + } + } + } + + private fun checkPermissions() { + val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + listOf(Manifest.permission.READ_MEDIA_AUDIO, Manifest.permission.READ_MEDIA_IMAGES) + } else { + listOf(Manifest.permission.READ_EXTERNAL_STORAGE) + } + + TedPermission.create() + .setPermissionListener(object : PermissionListener { + override fun onPermissionGranted() { + } + + override fun onPermissionDenied(deniedPermissions: MutableList?) { + finish() + } + }) + .setDeniedMessage(R.string.read_storage_permission_denied_message) + .setPermissions(*permissions.toTypedArray()) + .check() + } + + @SuppressLint("SetTextI18n") + private fun bindData() { + compositeDisposable.add( + binding.etTitle.textChanges().skip(1) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + viewModel.title = it.toString() + } + ) + + compositeDisposable.add( + binding.etDetail.textChanges().skip(1) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + binding.tvNumberOfCharacters.text = "${it.length}자" + viewModel.detail = it.toString() + } + ) + + compositeDisposable.add( + binding.etTag.textChanges().skip(1) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + viewModel.tags = it.toString() + } + ) + + compositeDisposable.add( + binding.etSetPrice.textChanges().skip(1) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + val price = it.toString().toIntOrNull() + if (price != null) { + viewModel.price = price.toInt() + } else { + viewModel.price = 0 + if (it.isNotBlank()) { + binding.etSetPrice.setText(it.substring(0, it.length - 1)) + binding.etSetPrice.setSelection(it.length - 1) + } + } + } + ) + + viewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + viewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth, "콘텐츠를 업로드 하는 중입니다.") + } else { + loadingDialog.dismiss() + } + } + + viewModel.isPriceFreeLiveData.observe(this) { + if (it) { + viewModel.price = 0 + binding.etSetPrice.setText("0") + binding.llSetPrice.visibility = View.GONE + + binding.ivPriceFree.visibility = View.VISIBLE + binding.tvPriceFree.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_eeeeee + ) + ) + binding.llPriceFree.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff) + + binding.ivPricePaid.visibility = View.GONE + binding.tvPricePaid.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_9970ff + ) + ) + binding.llPricePaid.setBackgroundResource( + R.drawable.bg_round_corner_6_7_1f1734_9970ff + ) + } else { + binding.llSetPrice.visibility = View.VISIBLE + + binding.ivPricePaid.visibility = View.VISIBLE + binding.tvPricePaid.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_eeeeee + ) + ) + binding.llPricePaid.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff) + + binding.ivPriceFree.visibility = View.GONE + binding.tvPriceFree.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_9970ff + ) + ) + binding.llPriceFree.setBackgroundResource( + R.drawable.bg_round_corner_6_7_1f1734_9970ff + ) + } + } + + viewModel.isAvailableCommentLiveData.observe(this) { + if (it) { + binding.ivCommentYes.visibility = View.VISIBLE + binding.tvCommentYes.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_eeeeee + ) + ) + binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff) + + binding.ivCommentNo.visibility = View.GONE + binding.tvCommentNo.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_9970ff + ) + ) + binding.llCommentNo.setBackgroundResource( + R.drawable.bg_round_corner_6_7_1f1734_9970ff + ) + } else { + binding.ivCommentNo.visibility = View.VISIBLE + binding.tvCommentNo.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_eeeeee + ) + ) + binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff) + + binding.ivCommentYes.visibility = View.GONE + binding.tvCommentYes.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_9970ff + ) + ) + binding.llCommentYes + .setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734_9970ff) + } + } + + if (SharedPreferenceManager.isAuth) { + binding.llAgeAll.setOnClickListener { + viewModel.setAdult(false) + } + + binding.llAge19.setOnClickListener { + viewModel.setAdult(true) + } + + viewModel.isAdultLiveData.observe(this) { + if (it) { + binding.ivAgeAll.visibility = View.GONE + binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734) + binding.tvAgeAll.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_9970ff + ) + ) + + binding.ivAge19.visibility = View.VISIBLE + binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff) + binding.tvAge19.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_eeeeee + ) + ) + } else { + binding.ivAge19.visibility = View.GONE + binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734) + binding.tvAge19.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_9970ff + ) + ) + + binding.ivAgeAll.visibility = View.VISIBLE + binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff) + binding.tvAgeAll.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_eeeeee + ) + ) + } + } + } + } + + private fun getFileName(uri: Uri): String? { + val scheme = uri.scheme + var fileName: String? = null + + if (scheme == "content") { + contentResolver.query(uri, null, null, null, null)?.use { cursor -> + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (nameIndex != -1 && cursor.moveToFirst()) { + fileName = cursor.getString(nameIndex) + } + } + } else if (scheme == "file") { + val file = File(uri.path ?: "") + fileName = file.name + } + + return fileName + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/AudioContentUploadViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/AudioContentUploadViewModel.kt new file mode 100644 index 0000000..6f0c5cd --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/AudioContentUploadViewModel.kt @@ -0,0 +1,221 @@ +package kr.co.vividnext.sodalive.audio_content.upload + +import android.net.Uri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.google.gson.Gson +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.audio_content.AudioContentRepository +import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okio.BufferedSink +import java.io.File + +class AudioContentUploadViewModel( + private val repository: AudioContentRepository +) : BaseViewModel() { + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private val _isAdultLiveData = MutableLiveData(false) + val isAdultLiveData: LiveData + get() = _isAdultLiveData + + private val _isAvailableCommentLiveData = MutableLiveData(true) + val isAvailableCommentLiveData: LiveData + get() = _isAvailableCommentLiveData + + private val _isPriceFreeLiveData = MutableLiveData(true) + val isPriceFreeLiveData: LiveData + get() = _isPriceFreeLiveData + + lateinit var getRealPathFromURI: (Uri) -> String? + + var title = "" + var detail = "" + var tags = "" + var price = 0 + var theme: GetAudioContentThemeResponse? = null + var coverImageUri: Uri? = null + var contentUri: Uri? = null + + fun setAdult(isAdult: Boolean) { + _isAdultLiveData.postValue(isAdult) + } + + fun setAvailableComment(isAvailableComment: Boolean) { + _isAvailableCommentLiveData.postValue(isAvailableComment) + } + + fun setPriceFree(isPriceFree: Boolean) { + _isPriceFreeLiveData.postValue(isPriceFree) + } + + fun uploadAudioContent(onSuccess: () -> Unit) { + if (!_isLoading.value!! && validateData()) { + _isLoading.postValue(true) + + val request = CreateAudioContentRequest( + title = title, + detail = detail, + tags = tags, + price = price, + themeId = theme!!.id, + isAdult = _isAdultLiveData.value!!, + isCommentAvailable = _isAvailableCommentLiveData.value!! + ) + + val requestJson = Gson().toJson(request) + + val coverImage = if (coverImageUri != null) { + val file = File(getRealPathFromURI(coverImageUri!!)) + MultipartBody.Part.createFormData( + "coverImage", + file.name, + body = object : RequestBody() { + override fun contentType(): MediaType { + return "image/*".toMediaType() + } + + override fun writeTo(sink: BufferedSink) { + file.inputStream().use { inputStream -> + val buffer = ByteArray(1024) + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + sink.write(buffer, 0, bytesRead) + } + } + } + + override fun contentLength(): Long { + return file.length() + } + } + ) + } else { + null + } + + val contentFile = if (contentUri != null) { + val file = File(getRealPathFromURI(contentUri!!)) + MultipartBody.Part.createFormData( + "contentFile", + file.name, + body = object : RequestBody() { + override fun contentType(): MediaType { + return "audio/*".toMediaType() + } + + override fun writeTo(sink: BufferedSink) { + file.inputStream().use { inputStream -> + val buffer = ByteArray(512) + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + sink.write(buffer, 0, bytesRead) + } + } + } + + override fun contentLength(): Long { + return file.length() + } + } + ) + } else { + null + } + + if (coverImage == null) { + _toastLiveData.postValue("커버이미지를 선택해 주세요.") + return + } + + if (contentFile == null) { + _toastLiveData.postValue("오디오 콘텐츠를 선택해 주세요.") + return + } + + compositeDisposable.add( + repository.uploadAudioContent( + coverImage = coverImage, + contentFile = contentFile, + request = requestJson.toRequestBody("text/plain".toMediaType()), + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + onSuccess() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + _isLoading.postValue(false) + }, + { + _isLoading.postValue(false) + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + ) + ) + } + } + + private fun validateData(): Boolean { + if (title.isBlank()) { + _toastLiveData.postValue("제목을 입력해 주세요.") + return false + } + + if (detail.isBlank() || detail.length < 5) { + _toastLiveData.postValue("내용을 5자 이상 입력해 주세요.") + return false + } + + if (theme == null) { + _toastLiveData.postValue("테마를 선택해 주세요.") + return false + } + + if (coverImageUri == null) { + _toastLiveData.postValue("커버이미지를 선택해 주세요.") + return false + } + + if (contentUri == null) { + _toastLiveData.postValue("오디오 콘텐츠를 선택해 주세요.") + return false + } + + if (!isPriceFreeLiveData.value!! && price < 10) { + _toastLiveData.postValue("콘텐츠의 최소금액은 10코인 입니다.") + return false + } + + return true + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/CreateAudioContentRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/CreateAudioContentRequest.kt new file mode 100644 index 0000000..f592958 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/CreateAudioContentRequest.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.audio_content.upload + +import com.google.gson.annotations.SerializedName + +data class CreateAudioContentRequest( + @SerializedName("title") val title: String, + @SerializedName("detail") val detail: String, + @SerializedName("tags") val tags: String, + @SerializedName("price") val price: Int, + @SerializedName("themeId") val themeId: Long, + @SerializedName("isAdult") val isAdult: Boolean, + @SerializedName("isCommentAvailable") val isCommentAvailable: Boolean, +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/AudioContentThemeAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/AudioContentThemeAdapter.kt new file mode 100644 index 0000000..45b936b --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/AudioContentThemeAdapter.kt @@ -0,0 +1,64 @@ +package kr.co.vividnext.sodalive.audio_content.upload.theme + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemAudioContentThemeBinding + +class AudioContentThemeAdapter( + private val selectedTheme: GetAudioContentThemeResponse?, + private val onItemClick: (GetAudioContentThemeResponse) -> Unit +) : RecyclerView.Adapter() { + + inner class ViewHolder( + private val context: Context, + private val binding: ItemAudioContentThemeBinding + ) : RecyclerView.ViewHolder(binding.root) { + private var isChecked = false + + fun bind(item: GetAudioContentThemeResponse) { + if (selectedTheme == item) { + binding.ivThemeChecked.visibility = View.VISIBLE + binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_9970ff)) + isChecked = true + } else { + binding.ivThemeChecked.visibility = View.GONE + binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_bbbbbb)) + isChecked = false + } + + binding.ivTheme.load(item.image) { + crossfade(true) + placeholder(R.drawable.ic_logo) + transformations(CircleCropTransformation()) + } + + binding.tvTheme.text = item.theme + + binding.root.setOnClickListener { onItemClick(item) } + } + } + + val items = mutableSetOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + parent.context, + ItemAudioContentThemeBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items.toList()[position]) + } + + override fun getItemCount() = items.size +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/AudioContentThemeFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/AudioContentThemeFragment.kt new file mode 100644 index 0000000..450a368 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/AudioContentThemeFragment.kt @@ -0,0 +1,102 @@ +package kr.co.vividnext.sodalive.audio_content.upload.theme + +import android.annotation.SuppressLint +import android.app.Dialog +import android.graphics.Rect +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.Toast +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject + +class AudioContentThemeFragment( + private val getSelectedTheme: () -> GetAudioContentThemeResponse?, + private val onItemClick: (GetAudioContentThemeResponse) -> Unit +) : BottomSheetDialogFragment() { + + private val viewModel: AudioContentThemeViewModel by inject() + + private lateinit var adapter: AudioContentThemeAdapter + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + + dialog.setOnShowListener { + val d = it as BottomSheetDialog + val bottomSheet = d.findViewById( + com.google.android.material.R.id.design_bottom_sheet + ) + if (bottomSheet != null) { + BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_EXPANDED + } + } + + return dialog + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_audio_content_theme, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + view.findViewById(R.id.iv_close).setOnClickListener { + dialog?.dismiss() + } + + setupAdapter(view) + bindData() + + viewModel.getThemes() + } + + private fun setupAdapter(view: View) { + val recyclerView = view.findViewById(R.id.rv_themes) + adapter = AudioContentThemeAdapter(getSelectedTheme()) { + onItemClick(it) + dialog?.dismiss() + } + + recyclerView.setHasFixedSize(true) + recyclerView.layoutManager = GridLayoutManager(requireContext(), 4) + recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + outRect.top = 13.3f.dpToPx().toInt() + outRect.bottom = 13.3f.dpToPx().toInt() + outRect.left = 13.3f.dpToPx().toInt() + outRect.right = 13.3f.dpToPx().toInt() + } + }) + recyclerView.adapter = adapter + } + + @SuppressLint("NotifyDataSetChanged") + private fun bindData() { + viewModel.toastLiveData.observe(viewLifecycleOwner) { + it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() } + } + + viewModel.themeLiveData.observe(viewLifecycleOwner) { + adapter.items.addAll(it) + adapter.notifyDataSetChanged() + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/AudioContentThemeViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/AudioContentThemeViewModel.kt new file mode 100644 index 0000000..1b70047 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/AudioContentThemeViewModel.kt @@ -0,0 +1,47 @@ +package kr.co.vividnext.sodalive.audio_content.upload.theme + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.audio_content.AudioContentRepository +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager + +class AudioContentThemeViewModel(private val repository: AudioContentRepository) : BaseViewModel() { + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private val _themeLiveData = MutableLiveData>() + val themeLiveData: LiveData> + get() = _themeLiveData + + fun getThemes() { + compositeDisposable.add( + repository.getAudioContentThemeList(token = "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + _themeLiveData.postValue(it.data!!) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/GetAudioContentThemeResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/GetAudioContentThemeResponse.kt new file mode 100644 index 0000000..b48142e --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/theme/GetAudioContentThemeResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.audio_content.upload.theme + +import com.google.gson.annotations.SerializedName + +data class GetAudioContentThemeResponse( + @SerializedName("id") val id: Long, + @SerializedName("theme") val theme: String, + @SerializedName("image") val image: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt index bbe960b..56987a8 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt @@ -10,7 +10,9 @@ object Constants { const val PREF_USER_ROLE = "pref_user_role" const val PREF_PUSH_TOKEN = "pref_push_token" const val PREF_PROFILE_IMAGE = "pref_profile_image" + const val PREF_IS_CONTENT_PLAY_LOOP = "pref_is_content_play_loop" const val PREF_IS_FOLLOWED_CREATOR_LIVE = "pref_is_followed_creator_live" + const val PREF_NOT_SHOWING_EVENT_POPUP_ID = "pref_not_showing_event_popup_id" const val EXTRA_CAN = "extra_can" const val EXTRA_DATA = "extra_data" @@ -30,7 +32,24 @@ object Constants { const val EXTRA_ROOM_CHANNEL_NAME = "extra_room_channel_name" const val EXTRA_LIVE_RESERVATION_RESPONSE = "extra_live_reservation_response" - const val EXTRA_CONTENT_ID = "extra_content_id" + const val EXTRA_AUDIO_CONTENT_ID = "audio_content_id" + const val EXTRA_AUDIO_CONTENT_URL = "audio_content_url" + const val EXTRA_AUDIO_CONTENT_TITLE = "audio_content_title" + const val EXTRA_AUDIO_CONTENT_FREE = "audio_content_is_free" + const val EXTRA_AUDIO_CONTENT_PREVIEW = "audio_content_is_preview" + const val EXTRA_AUDIO_CONTENT_PLAYING = "audio_content_is_playing" + const val EXTRA_AUDIO_CONTENT_SHOWING = "audio_content_is_showing" + const val EXTRA_AUDIO_CONTENT_CHANGE_UI = "audio_content_change_ui" + const val EXTRA_AUDIO_CONTENT_PROGRESS = "audio_content_progress" + const val EXTRA_AUDIO_CONTENT_DURATION = "audio_content_duration" + const val EXTRA_AUDIO_CONTENT_COMMENT = "audio_content_comment" + const val EXTRA_AUDIO_CONTENT_LOADING = "audio_content_loading" + const val EXTRA_AUDIO_CONTENT_CREATOR_ID = "audio_content_creator_id" + const val EXTRA_AUDIO_CONTENT_NEXT_ACTION = "audio_content_next_action" + const val EXTRA_AUDIO_CONTENT_ALERT_PREVIEW = "audio_content_alert_preview" + const val EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL = "audio_content_cover_image_url" const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2 + const val ACTION_AUDIO_CONTENT_RECEIVER = "soda_live_action_content_receiver" + const val ACTION_MAIN_AUDIO_CONTENT_RECEIVER = "soda_live_action_main_content_receiver" } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/ObjectBox.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/ObjectBox.kt new file mode 100644 index 0000000..d6517fb --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/ObjectBox.kt @@ -0,0 +1,14 @@ +package kr.co.vividnext.sodalive.common + +import android.content.Context +import io.objectbox.BoxStore +import kr.co.vividnext.sodalive.audio_content.MyObjectBox +import kr.co.vividnext.sodalive.audio_content.PlaybackTracking + +class ObjectBox(context: Context) { + private var store: BoxStore = MyObjectBox.builder() + .androidContext(context.applicationContext) + .build() + + val playbackTrackingBox = store.boxFor(PlaybackTracking::class.java) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/SharedPreferenceManager.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/SharedPreferenceManager.kt index 1e77cd3..0e1e9ef 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/common/SharedPreferenceManager.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/SharedPreferenceManager.kt @@ -104,4 +104,16 @@ object SharedPreferenceManager { set(value) { sharedPreferences[Constants.PREF_IS_FOLLOWED_CREATOR_LIVE] = value } + + var isContentPlayLoop: Boolean + get() = sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP, false] + set(value) { + sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP] = value + } + + var notShowingEventPopupId: Long + get() = sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID, 0] + set(value) { + sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID] = value + } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/Utils.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/Utils.kt new file mode 100644 index 0000000..7071574 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/Utils.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.common + +object Utils { + fun convertDurationToString(duration: Int): String { + val durationSeconds = duration / 1000 + val hours = (durationSeconds / 3600) + val minutes = ((durationSeconds % 3600) / 60) + val seconds = (durationSeconds % 60) + + return "%02d:%02d:%02d".format(hours, minutes, seconds) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/content/main/ContentMainFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/content/main/ContentMainFragment.kt deleted file mode 100644 index 5e7fbd2..0000000 --- a/app/src/main/java/kr/co/vividnext/sodalive/content/main/ContentMainFragment.kt +++ /dev/null @@ -1,9 +0,0 @@ -package kr.co.vividnext.sodalive.content.main - -import kr.co.vividnext.sodalive.base.BaseFragment -import kr.co.vividnext.sodalive.databinding.FragmentContentMainBinding - -class ContentMainFragment : BaseFragment( - FragmentContentMainBinding::inflate -) { -} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt index fed1161..e91a20d 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt @@ -3,6 +3,18 @@ package kr.co.vividnext.sodalive.di import android.content.Context import com.google.gson.GsonBuilder import kr.co.vividnext.sodalive.BuildConfig +import kr.co.vividnext.sodalive.audio_content.AudioContentApi +import kr.co.vividnext.sodalive.audio_content.AudioContentRepository +import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel +import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentListViewModel +import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentReplyViewModel +import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentRepository +import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailViewModel +import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainViewModel +import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyViewModel +import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListViewModel +import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadViewModel +import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeViewModel import kr.co.vividnext.sodalive.common.ApiBuilder import kr.co.vividnext.sodalive.explorer.ExplorerApi import kr.co.vividnext.sodalive.explorer.ExplorerRepository @@ -112,6 +124,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { single { ApiBuilder().build(get(), ExplorerApi::class.java) } single { ApiBuilder().build(get(), MessageApi::class.java) } single { ApiBuilder().build(get(), NoticeApi::class.java) } + single { ApiBuilder().build(get(), AudioContentApi::class.java) } } private val viewModelModule = module { @@ -146,6 +159,15 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { viewModel { SettingsViewModel(get()) } viewModel { TextMessageDetailViewModel(get()) } viewModel { LiveReservationStatusViewModel(get()) } + viewModel { AudioContentMainViewModel(get()) } + viewModel { AudioContentViewModel(get()) } + viewModel { AudioContentOrderListViewModel(get()) } + viewModel { AudioContentUploadViewModel(get()) } + viewModel { AudioContentModifyViewModel(get()) } + viewModel { AudioContentThemeViewModel(get()) } + viewModel { AudioContentDetailViewModel(get(), get(), get(), get()) } + viewModel { AudioContentCommentListViewModel(get()) } + viewModel { AudioContentCommentReplyViewModel(get()) } } private val repositoryModule = module { @@ -161,6 +183,8 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { factory { ExplorerRepository(get()) } factory { MessageRepository(get()) } factory { NoticeRepository(get()) } + factory { AudioContentRepository(get(), get()) } + factory { AudioContentCommentRepository(get()) } } private val moduleList = listOf( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt b/app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt index 7bf4ec9..811aa17 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt @@ -63,7 +63,7 @@ class SodaFirebaseMessagingService : FirebaseMessagingService() { val audioContentId = messageData["content_id"] if (audioContentId != null) { - intent.putExtra(Constants.EXTRA_CONTENT_ID, audioContentId.toLong()) + intent.putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId.toLong()) } val pendingIntent = diff --git a/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt index 7b9d746..934f657 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt @@ -11,10 +11,10 @@ import com.gun0912.tedpermission.PermissionListener import com.gun0912.tedpermission.normal.TedPermission import com.orhanobut.logger.Logger import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainFragment import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.SharedPreferenceManager -import kr.co.vividnext.sodalive.content.main.ContentMainFragment import kr.co.vividnext.sodalive.databinding.ActivityMainBinding import kr.co.vividnext.sodalive.databinding.ItemMainTabBinding import kr.co.vividnext.sodalive.explorer.ExplorerFragment @@ -189,7 +189,7 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl if (fragment == null) { fragment = when (currentTab) { MainViewModel.CurrentTab.LIVE -> liveFragment - MainViewModel.CurrentTab.CONTENT -> ContentMainFragment() + MainViewModel.CurrentTab.CONTENT -> AudioContentMainFragment() MainViewModel.CurrentTab.EXPLORER -> ExplorerFragment() MainViewModel.CurrentTab.MESSAGE -> MessageFragment() MainViewModel.CurrentTab.MY -> MyPageFragment() diff --git a/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt index 24d8f13..9807ea8 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt @@ -7,7 +7,7 @@ data class ReportRequest( @SerializedName("reason") val reason: String, @SerializedName("reportedAccountId") val reportedAccountId: Long? = null, @SerializedName("cheersId") val cheersId: Long? = null, - @SerializedName("audioContentId") val audioContentId: Long? = null, + @SerializedName("audioContentId") val contentId: Long? = null, ) enum class ReportType { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/splash/SplashActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/splash/SplashActivity.kt index 5943a88..8bbba0a 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/splash/SplashActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/splash/SplashActivity.kt @@ -145,7 +145,7 @@ class SplashActivity : BaseActivity(ActivitySplashBinding ) } else if (audioContentIdString != null) { bundleOf( - Constants.EXTRA_CONTENT_ID to audioContentIdString.toLong() + Constants.EXTRA_AUDIO_CONTENT_ID to audioContentIdString.toLong() ) } else { null diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt index f87d5e6..ae9ece4 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt @@ -74,13 +74,13 @@ interface UserApi { @POST("/member/creator/follow") fun creatorFollow( - request: Any, + request: CreatorFollowRequestRequest, @Header("Authorization") authHeader: String ): Single> @POST("/member/creator/unfollow") fun creatorUnFollow( - request: Any, + request: CreatorFollowRequestRequest, @Header("Authorization") authHeader: String ): Single> diff --git a/app/src/main/res/drawable-xhdpi/btn_player_repeat.png b/app/src/main/res/drawable-xhdpi/btn_player_repeat.png new file mode 100644 index 0000000000000000000000000000000000000000..715983a6ccb0f0d61eb9471bc0c55e40562fde97 GIT binary patch literal 973 zcmV;;12X)HP)^Qr&uMA#0WBt5&dbda&wq zt;;xB>zfhdeYjSunDx(Em$5ZFL$qC&`CmjuvVasH?I+{l~SfhwgWL$_aY~#D}yU0xUz4pQ|``aVm@^*Mp2Wc5!{x&tU6Bmn9hi@BD)b z`W~Y#^EYUHZP;M*f-6st${bmQ+E{j-B}qn55mjHkb}={2;b=|vRbBYbdg@X5S zokQUkt%z|ramNlHISNueTP0czUG9nu1uSe;9F=`d?D8#g-BG*;e(lnB!;il(pTMG+ z*MW?g8|?Sdtn5OJvxayK3uB2C_LK)0*gYCi?|TVrAl}?NdQTns5~aHC{j+E z2}G{ji3?dd`1(rOLRK@OfGE2amm0OcIwFhqZPsf7NglgSmN5<9xw?R0`I6>j4nUXF zAxF?KDIH28$)VaN*C#xYIsKr&JS243n+6Qn>PISm^ymD3KM1cRb++H7aVoS8B^ZXhLv-)P1RH5o5rwHoI4%lE$FINwh4}*4{NGKnz}Z` z843MT@!HT);xKN9c<_90@ni!0*n00000NkvXXu0mjf6BM*m literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/btn_player_repeat_done.png b/app/src/main/res/drawable-xhdpi/btn_player_repeat_done.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f3249f06bd9c220cb9aea0524be97919db1c20 GIT binary patch literal 1037 zcmV+o1oHcdP)7S+4Z!6+qjPj? z-Pvc$Sd-z+Xk>kyq$ll4yE_(n!|NhR5~lr!hlk6%ySsJqjG6?1`}=!lUuw15cT<(( z_fE6fJQYuaqex#bl)>$V}?BFc2C7B+!aFW}qZY;8WM0v$yl9q&tk;&$W22L_V@(z%S z$X!MbaS0K#G6EW^9I5h^Ob?OtuZM^6O-4YYtPv8jDz6Nhg1|Be{nBlyJD8iBYsm-* z#WLzbk<5wcI3$T+h)$9u5d={~a&b~5iG~CKn0|cb_VGqegd`D)=%z?BqI4wpo!kt1&Gqvu^VjG zXGltd)Sl+XX{_0l!)RW1-p|i9?#FB`pyf<47azp%$6IC zMxSZuK!SPJFwc#v(PFg8-4a zZHOxMAW}`e6ZIT|;mI6g(FfKVn7N{Z1g1{E4zi?%wtOH)OEWVwpRjXHOh^T~$WBUI zR34Dx6xR^%y<)-gZm6$XgcGBeaoK=gOgtI`;x|0nBU*AP*=QU6d@LH!i}!*_Hl$pi zD*>cVv(q$h>6w&YjLW7^A-oywMK?7zp=Ik^#WA_QBORT@nCGaY2(Q@nd~TqtyRNak zdM7@~mRvwxF_o8hl+TW>GJInwmA2YE3FHjOUw657;r z;j&zGjpseWP)0U0--ZF5abtVOrmABuoY>6MtF!vV_~zU--oza10x`a-8Y?!|5udiW z=vE!3&qP78czQZ~XPK&;sBcU>R6?@Q8(LQ#pZ`gCLjnE*MfFcJ21Q`F00000NkvXX Hu0mjfDbwD% literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/btn_audio_content_pause.png b/app/src/main/res/drawable-xxhdpi/btn_audio_content_pause.png new file mode 100644 index 0000000000000000000000000000000000000000..157293d7abe4f8c1836a75c21518177844f988aa GIT binary patch literal 3129 zcmV-9494?`P)OF@cDcPXy8;)?CAOq()gLK^=8SERUuEpxEN zE?hCCi(sqR6)qMtMX&`cw?~j>T zAV{s%YBg%LT5~uY*7>2_Z(Lkl)E5>OMvvt+a{uesul1;ZpPw7>bE6-*uYT9(KdC2vS)fm`Q@nH!g>+$zg182%Dq=TonF$d-5UR(EI#B>V5k3sgK~# zhJZ)lKnG!#TX#|gE}QxWkdPlpweNH~KEV|TIDM>2mH|I z!1t-*DjRUA(u<3WE(gNphb~!2s=Eq%r>bGERKQ)=t*@-CY}*{_NmST(sbZDA5&>5| zb(<;{>Pe)k%a4?GvYfR652`i)kK8u3r!GLBzs)?dxT;$ha2YUiZ{0$$li>kshv(lt z+k$RQz}2Eln?v0qNX@pO8y|24J0G?L-PnMm$1VmB&b=>Rz8E{rXk@@QH#fJr!n6_W z!i)x^CeFGT6>v=Vl}>{G%A{&;M8KsTSz20Z^WAL%9qgW-o_fCT4@h&`2V7O{$5im{ zJ(x+V`D@)bcxfAORBbcJAoR_fHy&w9%Ye(&9}gn`uJ&Dn{pd%tvIKo{`Wre)ZfFs=1Rz}DR3pm0J zqUewp5^#hY!io%e?g2-*Axcpp&o$r(H%wDR$a4$0TEuxo08>cFdBwAz*J4mE0T1?; z5EgMD9(QSJ>GzY9laG|K%mc2@)cONq`5j`C6;F0IkG(4nWlV(|d$}R0(YL;Ao2Cq9 z8t~1{P3gH2Zb(J8ORHl7GLu=0B1huzO`8Dbv%a>rHpuG8piC;b8dSCkAOk$J(pJ|+ znp-9Tm%bZknS!jymFcqT$L9{XEanjUZpc_x{rFrxxSW)SZypmsKGu2a(C4Yc)W>$F zuHf?Zq0Tr3kQ>R^JgrLVfUB<$v?h?Z#yqV`s(`BzF3w*9McG#EaAK+Uim25}F+-c zSpaG@8i~e!u>&sEyQ)X$NEIZ5ng0I6ION+&mhoc;TrSl|040+uo+OL00xq8zT!#{L zAz6%D#bX5=)f;pZH3XX*xFqJ$0i|+!? z?Fj~#V5F!vM z1*bp;(~a*NutX2Gs?}+=% zu#!xFr@oXofbL{}m4ihdu$6Qt4t5cO%}~L?TB8;P6&!4ZK_*xa0SBug;9xZb9IS?b zgVki3a6kYS;u*q$OcNsDU?nNuXi>mgqYBQqeFCr$S`@Gz0uDCoqeTImA>d#y$8iQu ztyV*e0#>@Xxac{<;jl*l7CJjS8#tX#2Q3O%soU-Lf`uN09IV2#gQL#G2|oa^MKJvx zoIV*99BkvuKY~k`pn`){g2|Jh2lolUA{_8hS27B?%%osnIM_n^Z<#@n`kw)bAq$-e@!;UksozKkAo}0rxy_Kxk9I z5Hh%&bZqg&lW%gH;t_xWqGnG569bODDPV{`XSFlw$*HGL9u9}-#X)PcI*q9Tm%%2g zIB3qF_T;Xld9z+HGvM+GkxvQ%Xe3Vc7ICl9;}L*f zyf`n46>wB>&`I3tEs|Vi&BKfeXu^y8!q@?qPmE17DxW`pHu}SA`uk5y=Dm}g@;lLW zGHsd{fB*h{>;tS14-d6Fl<(cUcSgVelqA@RTIECmmw9ns_xH#G&!0d4Z=5P9ui4qz z`Gqv|;>8Q2-+vlOtGB38LdVC)Z`an=T)x{NgPok5$oVsVyLa#2&84NKmGS2#7`wI^e0P?rX)%#aUAyE$=cH_nkk8o`t$Y1~T^wgu2QU_cHocy(sxF-Tg zLNez2e(IUKR4qyXtqSBx&6>=$f}Xm92c%WuxpADd6S5{p>UcD#xdX2DjQ@ybe8`Gq ziL3W3%D`MLN`PCH0k4S3Lx6XwSuxqeoCcUWdENo zIy(Bey1M$2-}nXr%)sIPDSY9mEaeh#!IiwnA(v05O8^t7#hly@q{%Jdf-~+TZ} zw?{e3HQ)jvhbTpaTjU;afsjL3QQ;Pa1Y98GAc_pPC@kOtA+NTg!z~I8xIoCOsf=)o z!UHZ4@@gq7+@h3#3l;Ld<2Wv1#nVb^h4QC7=ecJoE#N{e2K}m5JnIBD>DOwtfA8(> zWwPxxtx^LnWbX=x`~V@hhg_7Eed7CQ$s<}a=-O6>#3DsOTasS*qh=2#s zo0p3#w$C@7?o~U?sDKAJ=q=ToBj^Rtb%rDkBLgm^n#&Caq4|+u%XOLh{r;{A?MM`3 z10GO8m)VvUL1xrtBRw6b{*o|0-~j|Z8@2>HU`@aS2zqisD?+2IMcNVUfOP>6sGz$T zj80}lk3EaE0S}W|7ul_d6A^u5;&fkHl?ZqssOlVa+0fOb3Q?iHsE2Q%Dy3o(3!ebSM}6A{%<(oa39zL{&@RQh_+Z4LkZeYv;*|Hv*rxyXQz3OySF zJ_iZ@`T2QWReP!O4fP|{wJw#PzX9r#z5!HA5e)eHYR`u}mf*@kyQ7u^K~C@gT?yP! Tev^{*00000NkvXXu0mjf*pKR4 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/btn_audio_content_play.png b/app/src/main/res/drawable-xxhdpi/btn_audio_content_play.png new file mode 100644 index 0000000000000000000000000000000000000000..cc64b645562bc3b27d3a0c338edbe4608a34c598 GIT binary patch literal 3911 zcmV-N54iA&P)Y|ma zRk+}EWe3C$81&3WvXF7CQburb{Gg3`?-**O>Bmfcp7cFqk~vAvIXR#2`Tt;=WM-<> z{^rU5ecrDz0=}tKDy4irUtU>RDe_BsURqsUEsl+iwO`Bk$n&dLuNIwtKYy*oUu%EJ zbM?6<|MQx;T&~6ctj48Fms+P!pKcKF#TbF1h{*h4uykdXvBVOVNeLVk{O@hZ4*^SW z@efkt!i5V>h(8<%?f?sI%PfyZzX}{T>K#BNUy^FSxVUH$=mLV%wF=E5Pw>+Tsm@6i zdKh+v=Rf}vzqDBRCS{z=3NBT;P$(EIguyQj(h#ZcO7_L9%AP5~4Z|pI+O%m(XQ}(4 zWUo`kDtjgbSDCs-8I!snQq|={$~tL|wu0N$n*SoF(weCwpvmuMCTU#NwG&(hjGV12 z5ZfC(KwaeiyQ!P#+6b;DU1}_Kg@A8#6Fv2UL+o(SMfB7Pj*J}*JUBP6T)C3kHe)IU zuU4y5Tw!VuJ2KjX(KL2lQYARreX%2=zmukFo(jPwj%?hxvBpoQ2qb~}^7682Syqb@ zPT~buReLW*ym}7|MXLG9q%ZJFtl+5H=|BddUwrX}NeLy9f=la<2a)^Kx@(Yp5}VEQ zlrW@2!d2lBLL^BO(#@*ea?34O&YnH{6D5emRB%KRZKy9SEPPA}ArXQjl4wU_B1`NM zWpZ*-r2Z$%y@`M>$j0l5@$vDcMxzmHD=}8VS?&t|q)XnuiGXgjb9uR3KF^ZJwmCXB z!SB8I-Wtoji=P$iQbZ@rbyoH=7r48$V1lDkGA6UgZVaY-Jz;E+3;#3gy8 zfaC>bDVG;+p!`QfS<9EOQ`s;ZLn7{>BC$&x!rr&`}WX6-ZBaF2xISLph zH`a0kue9IwW!W?ZNT7mOt5wO|kQ?|ymP@NW0f7-%M3J31{7@qRMq|t7&6`_6?FC?Q_uPN~{ZsjT ze%!bZpForpZ@A%x0(VuuBOmw@Ty;$zAx(Jhx#zY&^w2}KLEm%z z_172gyYIfcwr$%+b8~aw6Nr*6933s5<@Edx>T8gxTkhVubEoUWhIZ`OQTynlkM`ev z_uUv+M~FH$+cTakI9KlqS8r{u3z0$PQ%^nRY1QM6H{N&?B1`0y{7e9Os^DrNRgE-4 z1ScPC*s!7W^wUr8lYuFLNRSC2uf1_kg3I-Mgr0X7h%C_|6F{X>$>xn0xxRXGru;f!-rtw`1E?4yt=nEoCG)NWq;-V|TA;RG zhYlV38-Xr**QTrq&ea=(%M>AzWzU{HyAWB{)9G?&L_z*eA&4yNk-InT=WBw?f#U>H z2_nln4Esr2@X5)^GV*u2L1g(0mBZWbXl=n)S6A`&rU{5FYZ&rNTkw^Y6~q&bkjQfU z`0)c-jx6>-GpAk{1hhjU%Uf@~wI_>_g(LT05k)c!0WFcewONcTcGILSI2KsxO-3Wj zwF)jB6atwbYmudWVZ-Iiml01gM3y3pG*5DvqR$>#i7feizL?`4I4XEXu_KGd)x0Ew zOic1*7>O)$%9f^*%cW^K2%d55$fDc9pwj;KbrCGk6FC@Er!I@EugYPP2XrEl<>=9) z{~!&Jbdtj=LeR_Yx8H6&{P4pDX#grXaOBZPA5DD=u^mxR!GRNHhEfg&nV>HhFJ5es z20(D&$Zx;>)+7yp;J}f3z5Xp}00al?`0A^#7I}7aku(5;gDynw7hZVb6VikNk2G88 zq<~A8F10@V@Wc6+UV5ocn!sMS3i1m-hu|6J+_`hj7hinwglU=$(g-Qu1p=AIjwC<- z{IeEq!A{y0oFAnfgcXGuL&r$c%UVR?QcfT{NF+IM;J~}5PMy-C*F6Xbo-scE{PX(Z z!-wZ}j3f>;?T7+2N2cB-lCa#`MiK`gcovXI^6azEzWeppUuWv?I+x3}3i*6q`rrig zBx8}Ji`CWDMqy=TrNO2X(2I;lk}jlER9IYGY*wpP0{W2ENYcyc)2ADD=|&FM1c@a3 z_U${Cb-6iqw`XMw_6u$mVzMX2NFCCqP2Vr+mgthhHJkYcV zw;d-Ii6qh`(Pgh>nDBUV$$q{jI0l-@MIwoGNwToKH3-CI`}vyDoWm{I;zJ-&NF>?4 zd-pNO?GDM`;5=q{C^Uc}g$B!=&@PgI(qIXZG{LeH9#0nybf4E3@ z6@;$P&DjOjB4!XtM#k!V(IvR_!wHi%sK_m%=-gqK7or_`-hzyA&sM>+CZjNAG<6E^@ECQi|ND?eg@kDSu*$OcwTz>rV zN9)j`L$in^!NGz%Zq0L?x-6ASncc@1HWEqp?%kV-Lw6#^a{8pB;HGJ|2r(s4B$6CH zeE3~NlHifSrBgf+`Yt}D+1OmsYdt%d9aI8%U21Jt3X|k(* zS6B`_K6zzjrOt!R8UfEpBzfhPS5Ba6hs&^}F>rmX3^q~4y@N;+Db}Db4isDln^MII zxCW6VI=Ff-_8VNex{c!GHMS2g}RvK_rPC)9p9r-F{=E(P)j2kITie25CmQTpruLeY>k8GbEBc z`Q($cI4Uh3r0>nu+x5;fPv=DF+839zM&!Q8LEj^hMAniZZ^w$n5>E{e*A-l_EbGeV z&6}_A(>q8rW@l%=-MV$_*tTukjNY$FB#|9A^Yims-2|LAxO#uwc_f4zeBB>dtyZT= z)7|$R!-~6i?>26`?Y7eIzyE&W(@#JB9+NhSpt-QH@G5!4m*Ct{F}R~LO#nm$SMOKd zc1}9+6j5w=z}e)eA}7&qAppXs{^gfnenUR+eHh_}4I50t-98|En#;?}Ci%p-;Ia^l z-!@JX03jldadX3W=gwC|u|ZS;VWN88BdvoP`MhL_D%=}K`wRq)?9?%7L`D`|tr?%e z#t0A;;u2TycPIcxiYPWRjj!tj5Qv32?wWi@0T77b!r8NDe?lGy0Z``&9g_kg&^t?z z$H9omY~{xkAb|>Q=W#-R9C)W0RPOF1u!v$Kd3?*3Efzn$i`?%R0vSdPD*xkWDt;J& z3oe{FbLRZ``1pDLgX3C4Fcg;i1Fx$D-4cr60$1_|OHMj&kO0A79tZ|faKXGYPp3-1s5QB zHi<)SA#%Y5NS-a?l3R#Ha6w7lRJXZd##2Mo4CM!5_PK`;|hRko+2b~^WT}tRXb5v-Xuy<tf z;xH73j}7mk1UJ9{6|7Uirh}bSkWB?K6)al;DIuulvWqtRvV5zVAnX`i^R^seLKna=CoK#1pBn4Wcq03&H9=wXsJ}!CP!0ZL4E&9OKczz41c00Y za+m{m3%E_y99R+)*gxnxf&#PAf@NPfazPt5aUD2{00bdW8#qe&igjIW&VO9|Vf63@ zKa4lHmj*H+o5B_N4EbdeKoEyUXIzz=${^doDf_jgqsUDpUH+&fDysY{E!76h6r%X5 zgGDAj^%_j_6+o?=*a&bNgX47S`0~l60TXg2Vfi4LzIk5rc>+W@lN6y&h!2tJ8|4|F zCqOLD7ZB?xhB#3|xv+RDiE*NUX(<{|?Y?C90(PqN(1qZ7>d4c2)~PPpB*s1!P;e+JH8YHgJsq z+d$gFH3n<~=>o15U>!(TaIFE$AX#<=X9ZXUseqioo#qrvWg$)3!co?MR34-YILZki oWkR}wb1q)n`M;uEbya=+1;E8aP$Q12`v3p{07*qoM6N<$g24n$NdN!< literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_audio_content_heart_pressed.png b/app/src/main/res/drawable-xxhdpi/ic_audio_content_heart_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..bf69dab86ea4cbb0fc4cdb91e36d0c3a74f5f1f5 GIT binary patch literal 606 zcmV-k0-^nhP)jK~#7F?UzeZ z!axu}yCYPw6K~)V@CGcAjjk;h+`#e#Bqx9)WPuh-+$dEp#1n`o&?CUgDt_&B$M6gJ zhcIRF7R+Q|reDpFnI3>6{~Qea0X3>+n3t|$juk*WbsA|51`MD;L$v$fiTWdbKoQ=< z`lJ8A^$N_WrK(j3G(a1Q3u`+NHEQQDMN>t%Qjg`h{R0K*_VP{@ZHUeY2rPVfX|MG+ z1I4^5CU%}JPb+xj>P_`&LL$_RIir}62(RRZU^=;sAh>r|f>{d6e`bbXpaYB91wOtG zxoRvMr|$A-fKh0~Lb%PuyN02DSew7nM za_ShMIgTLW2y_E+%*CG|4M8`I#hVoJa8l|4pQk!8!A~wk5bd5i?CA+~fyl snFunQlG(^*j*_g>Opo&O8@`> literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_audio_content_share.png b/app/src/main/res/drawable-xxhdpi/ic_audio_content_share.png new file mode 100644 index 0000000000000000000000000000000000000000..973ae15daff6558c85d147d7bcdc8a7bc9c48098 GIT binary patch literal 708 zcmV;#0z3VQP)SLR0D+eT&o4C33wn4 z$f$r+C}ea%Dg-hn;HrG%3mFY?yaNU$_b@62{q>tnzM^o2a>Vz}WBMnC8h zsR@U+xvSjA-A4gAYKz6<4V|Csf*IwT&1P_{@?6N|2wdB^qV1?&!PqMxCdkk6z?FEY zuDe}v!ig|*z!7ZJ>-C;RNp&C(T#OEnHXOj{mcAw&RpoAk*X;qIDZIDrOyJHd7|SJs z2#L#sql!zJq*mcxka9LM;CzLPb_?h~OQSjI|n##w}i=hjf9qG}?AO81-{A0uu>@D*(|r zvl?=J%dv1~$TKdpQk_%@^^B<(q)8}T0Z5~G^P&*>-0UKm_^G7RO+fWC0lPaGb3!%?x1--6&MO+fpr qI2ex7JOUn-^BQpKigC*S=k*0TjNUApYF0l000005Fq0`>s?*bns_o}&lo9r~$4OGSy8suWrjgJS2u z`%Jn{_H}mNb~itmu1S;C`SHx_&PD`7ngxZPx-nm(hC3gK>7HsNcWs>`1MV% z*ZV^b>5*kv%DPBvA`M0AQ+*)m5?3K?N*1vqNRhHic8O#OQI})^qk>q>hDe)aCQ_<^ zNV=d{ma(WZBFM8Cl}MD3bVdfKEl9{g5gJik;f4f6{j6K5YYXD2c|md1gv>;wu@H4n z8fXa;au6-ADZHr30@3tMr*5h&h>O$?`LAoLf(Sy9`(5Qrl>`YnsED_z*_;STld>N! zEl9{geX~XFU|hD9TPpJ*j-0m00b87>LJ9?m8=*Jc^kS99HrJ=Q(^94Tn9k&K{qS|C}IKSYAKa<={Sp-HUDVkRTVwjA95gGl>SpFNR2k^R|)s6;(UzOcwU zP5>D@AU*c!zhvC!J=p`rY~m_{#)+R10S8o_q#X60HW;Fq^?3|pJA#h*y35yB6i2|9 za$ld5gZq7?crxA=2%anavqEk-$eV?k5e98gK5HuvWl$GRL z4j4d?j~T-vNg>DqLp1YG$PZGG6tTzGF@+%~q9ElIq}0i~XcSw<~x67Otsyb#lg1w}-4sdP{bR{yy2vW96sKY1hvbuQkFiJtX~NEx)@<>BAF~M0-D? zMV$PI;|(oH@)hfDGwmQ1{F|&Yly*QFKh*BQ?*d zQ?9&?vGkUq7BI=$x|ug8jd1HiQ9~XxSBhi`;-ZFuWt&^IJCTm^_Os?b)!-WaJGGbN zqJ6A|kA1joo>2QZEwg{M)N?W3<7s50ejuEQJeBJ2b#YVgX<0cyXInTHH;$-z8|_db z|EPKrL!E;Ku+DHm%>YgLk&}Z3Bs>ebMJZc|0>k~$6iIlM>`MwSnh-U|-VQfW5uy-H z&&aP)wUU$2K2PvVX}M&oFQ1H}5^q7!$qdA6LSpQ@vidQRHzIWZPn~_>oK|oo=`5 z1ld}f1&<8kw^?MhD%WZYqeSf15|LE+tba(M9p5W_vrtjTCEna zh+Ev+v|kUnS~^fIAUtNFc*13oxm;WqauSSjgW@iPJ&A?3Xdj?hHN*M6HpS(L(xO$k zxB4LT7qDlzD-FN|5t*0h~!l$xy@L(7_lS5u1DujG_IsSS-Fm z%GjdG!gzzE(r4{GaN%YCfOyQ@@@U>0x_-BD|ykIuub+7Ur{c zWF5OfAt``1whNQIIhNqHXQi6g0{%ssdVp}HtG0^A{wxAZUa77Ck=Pp{<>aLENj+Vj zJwQ!zexx&qPbN|}>vTH(wBBamwSm&tMrWf<`cUFmGI4B5x$SoQ>l#q(L9VQ$bAw+T z=tRn7GM`ED9-`RvO4%U7e685z_^sv?ka#E=PCaHPolfU*1CS-&SX!rq6mVBclZ)$8 zR#JWg-(^b07*qo IM6N<$g3c~;rT_o{ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_message_square_777.png b/app/src/main/res/drawable-xxhdpi/ic_message_square_777.png new file mode 100644 index 0000000000000000000000000000000000000000..b4a2ed2ae1e56c696be9e08d087ea3a47345ec23 GIT binary patch literal 471 zcmV;|0Vw{7P)@QG1^T}T)h8tc$pNF* zPqK*rC-}+O$R9xS$1%<&f*^44@(kniRaI3QMbT?l@|PfrE%CC!=D@UM*dD_$%z03B z+_;75<%kv36VswKgTC*(hGA@3=zy(r>aT=-6kD>`8fP(WItzX`$~ml?7e#TOB*_6r z#PdAcG|e5A=W+`e2p!EW%W~n$;A#2}Sdk`#NO^vc;IVFa$A_(8Ty{9d z2M=5nY~-M>zQ$x@?XogI#hNmZIw^s=bCj!P5;f zxg0U{GowU~7$T@lj<_VKRE}r~;!ffs%H@dGCPASW+_^u$Rm*Aq1>ao-gNI+T%$)!L N002ovPDHLkV1fW?$ov2R literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_noti_pause.png b/app/src/main/res/drawable-xxhdpi/ic_noti_pause.png new file mode 100644 index 0000000000000000000000000000000000000000..e98bfdf1461ab2feee6c696bb5ba7f90c08e0f07 GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBIe$+V$B+ufx3>@GHXBH^Jv`sSn5DAAOnQpo zA(dV$$6b!bj^P<v+3~|dlf}AJq z)^9qWXjnS&g>Pu6e20K+f<&92+}E#vOniMKr|Am)=)ALbPgeJ$3;{*wJyp3u|5&3{ zlnh@>uP^JEqI6*YF@}IX#sxCW4wh^UMLY~oB^XW_GE7Nk&^S(l#8Qq6>wNRfX0Lev zW4aT^bLsU~K$kw4Q$6i?s--%+T~zjD)*0_*&t#wT)?enHd`4JFb+^&uz4uRlh?}=O uL|k><*V_Hl<^L)iULO*k*3sdyIsbWY0@pT4iEF^1X7F_Nb6Mw<&;$Uz*{RI{ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_noti_play.png b/app/src/main/res/drawable-xxhdpi/ic_noti_play.png new file mode 100644 index 0000000000000000000000000000000000000000..c2c27540e34eb9bbf3e267b1f6a23e10b2900330 GIT binary patch literal 670 zcmV;P0%84$P)?tb|Ab8ZVWQEP!xc3F!h9E-Mj*0F9~yreO#ciAey_#59bl5}1Z2SRgh53aW&D zgDjyYQ3}vlsYJ{Ih}xBiU4WuaC6W-JIja&W2_U{JVR}Jo0u)pUlrSm!tsvG03-j{ z>2!=b&xu{Yza!6yS-@G5=R_%>PUJbU38)=;PD}!{1v9KE>@8m$c3XPl!waMIL9}N#r@w1UQd8M@$8LMV=!f0X`$o2`#{Te7sL;{j+8p=*iMd;LiHIOm8Q z$8j9TaU7d8#AFzTLm1xT*YD%;*pni;c~}C%WZg7Pzu{vS|J655^Q0wVCC_8X^SAdm zT4FGcpji6GeSP=oB zWMIkbOECeUL~!XlOi=+KrvQ!HYjFV}r4WtcK@|ajQ;=5ipqc=%r!aibDjrl70JbE= zaq*Z$uc5jCkg$9eYqW?601OGDT_!OBV21?p7F&sq&|TxF0106FB53+HX!=3O^rNup z#gOUcu<3?~>6V!3rl{$*xak)oreBTS>dR4#VP4|EUJh$Lu;4f6A{P$NnI1wu;=mH1 z`N#@(Ncaikff(fO5tzY~amnQx8{{H+uzw&pGFx^ZkBLj;Fh$sE~pX000nmLfCn8eIHkw z1^BrZ{4LF#>jYyEei#5iSn6K|0`6DHa}R+SZ%11|(|e^QZUBm~akBvcCttthCIPteb)-l-LYaT{iJ)XCjVP{9J&dPpX4;s5E&FccRi3B6TC$%Jccl@(LC9kX z=dv=qyLC+0zPph(X9{%u+e8Qu_K_L4S3R$Am-GZ4WPsWO^R&GRJ=xs2?z9D_#4 zi-YgoxX_!`nG-i-=w5;c+IN4sOqHNZ2H?8?OT ziCoaG{7A}hgz&`+Zx_o-W)f9PnR2#ff`G-kS_36`}ng}xut ze^!;(yGZXl&@E2~+aH(B*P+5+dpny7>miJHr@2a?%zEUB3i1RYiL9_Yhs)9H9&cJPhd)WI5sSL}93up$t*#RD63? z5a4ST@H6z*G^}5ANgYggWcO-JX2jOYi7msH)ED48D7bU(N%akhdl$EJ^1)* z?K%$Cg^=#{@61%ZspU86PLs~X@5#cGqIoPw#a%4evKum^^1?~Cq|ise#T2p}5rX(Z zOO}+~E}Z41hFVDopOl0M;P=k-%Wb1wEbxg}oaDCe9z}@Nt1nR>py9}&h@x1OhY?hF z-q@v;d{j^q3dLp19YP7YolNxKVO#qOp)9jcri0R$Bb7YTtkgWJk%;dww7{MJ8r4ALl&lQ zf-8$M7Qx#|`2sYCO>Dx9B}8oco?QUFMmXDpwm<1+w!;=pI6O&J34$jxV9{YYbA?** z^ctb;=#%p=`~|Wj245~>={17c2DElvilC8n0IJ|p`s^y@^#fzckKrF>Mz)bRzau_j))4O_Uc( zT9i7HwcvfK=&WQi3StU<7&yR=&5@x#)_o|qeV&*4sT8cfNL4|@ZExhg8jRRWa|eI| z=o!KY0b2Xej@s9Ip2A7lQb)j|d{`m#KB~$YxNUM?B9#M$2vEpi8GM53ju@Cn*z<>;ZOY zx?SuqI)}=>0=G4o5C0-uHnc-!Z*c1h%DFUT>!bcf4-x3>fs}IPTH*-q>tI?pR=W)! z!Ra$qeWS;k=F5yO@%6}T+X$A$l6WM2U72pQ>g6(Fz5Hm z*z*!Q$^(n-MskuAfgTd}U^EG5GItiWqXyNM#;j=g&Kk^Z{YB;`JLQ?%%{|#U7mHz-L?_RdKFq(+^sSB-JDaELwuU5)# zyYmp8VFpPaYYje;B_r{E^tSs@bIErt$h_;m^Z*FLnbz)#$tnK6;@YOeY6Ks792(ps zIEFlmdoz(%o1)$38;>xHYLwfaA9vr{9Km5Hp`uRQB$lNdCQ}hO~)Ki@s>8R zqd|cRb(mnfv6;8@hQ?@60UOk@dE5lM2hrWvaPZa6YE{%#wQNi^i^i$z&O@4?B$lM| zjUCr#foGYYs)za|@w5v6`3CYca3-zd^-MjvtrTjW&4>`@xG;R|jzh6GVtOlHhq5S7 zPT4=_z6Ouj^p8kK69XGBVFb+b0iRb^uKpveVD3D~?=FPIBoq?K|L~Tnc|K}wS&tnF zG^rtYx=pD7tvDv+%Oe-1)}&54wykUvr8$YK9_P10+DShfW~+k_O1q2|6ti>`ao9v! z%=k{-(OU*7%Jko5KbSAb(VK}|jUV{cGrT>nnYRZGn@pRH`-keKNA9hoX3CjWD)~m7 zq_-QV1&gWSI(4J|7wSNnbIBHM6~B7$ogEpa<&_rOFjkn?<0vzG>6rBA=(VX9w>&3S z7`^vV*7WH@GUT5UE6nJ8yka?HrA9JHw~;zI5T6i%+k?jr2)6mp1dAqMpu#j}>|?^GdL2{wuekf3D0p~Sbvj+ozrgfh&2?w@>L|L(?zDnNrAvb#_U}~| zJJuHG9=1n<*~p8p7s~+;ubq@NM!TKIPDpxx&ROvRP3z$p_Iz~A6YYFWCR8}yCw z*0I2;crrJvMWXm7;f@cw-n#N*V$UCDT%zu@ajH(ub=P{3-2ma8jsjRQpZUANj}m-S z%Ir^=eq*HJdhKsTtx<+k2)|o6gvE459K@npGxq2I%>6Mp0CLQkDUD68OHKhR9mt5J z3w+6Os4Ea_o>LUa?w609@4wdk>EL`ctbx)k^ShQi%SkYKV}JAM5i3!?$mnWy#oMHH zH_VR~@V;WI@=c^EbgpDH_Rw+bn&<7!q81q`3ZL!C1BI|>_MU#< z??BSZa7ECuFy^6!+T|Ez;}1p+$nWZ0`}YzB`6^1} zN`LD_8d5}XxNeJ^0PeP!GPb;9OH~Z8OSh=}~wksGI22>{;>EAMjy5>X2&R z=lTVP)@=C_i_OL^_&kt#Kq1+IK4jc3X^Ou5?abBU_nL?@*CKU$13$pyt7gY%b2dux zcu2&YJ-jEECzXe#somU#EIMB?t*^Q*Q@@&a4oHbQQRrc+1@6ir@)%^m=7YFowXUob z?k7z}bqaQGPN032++}c6xTo|5P`d@yZ*=O|$nh-WU}YsWpI;^6#;@z9G*m5mOl&~4 zQFRg6FG_BA?f1S{4LA%Ma=a!2M4n#3s)^j*=^Ku*@1a?v1>bcSG4y8h7W`$dUhFSxZ5?r$==6VX485d~tgSWHHv(E`Q z?65!h?&wZ}>g333zt$M&3ImlbvI&!f{3ShiOIyf(f$R?VmHJ7Un=jgDYNU%mhazjULvL@fRO?KSUS=J{x1`}be(US{e@bbBC zlNmLTdp1*axXNr3E( zHzWBHWC2w#0v&9Z)tkq<#RB@%fxFxM3g1YSC}4n=%)wQ*^1s_G>Ti?yn! zE;FAH#7y4VIy+(-5VCZ15PMc{V^gelyKiL_5rS(q-qo!0mJ3&YoGY^`P8u4 zjB9L*n4BaADLKsN0~f(+mQB9N1-&C3C|l0liEC^Nmx;GZC?U^GOir3kCtZhxW(OcD zf2jXI(I7POL%pUnJSx*8>VB;MbeXu)Sd;VY3-v<*RyeVii{M(-QwchULZ<4atHB9D zaRpUl2{L(kUozzR|u5DNL) z)V9%4bKXHmFMkMN`yk2W(D$VpXWn*VwdN#}2Xka?{i2bCsiZ(icF4rEue{Fl{BxUJ z>ruz+f-fD_r-LSO)&`lJ^BW2AHMvcjX)U}GchMv#H%aZ6z1HA}#Mod!$g=}Z9oHj! z$#E`x#`eBhkUt<8 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/audio_content_player_seekbar.xml b/app/src/main/res/drawable/audio_content_player_seekbar.xml new file mode 100644 index 0000000..ff35c70 --- /dev/null +++ b/app/src/main/res/drawable/audio_content_player_seekbar.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_black.xml b/app/src/main/res/drawable/bg_black.xml new file mode 100644 index 0000000..f3e1bd1 --- /dev/null +++ b/app/src/main/res/drawable/bg_black.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_10_7_2d7390.xml b/app/src/main/res/drawable/bg_round_corner_10_7_2d7390.xml new file mode 100644 index 0000000..1151640 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_7_2d7390.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_10_7_4d6aa4.xml b/app/src/main/res/drawable/bg_round_corner_10_7_4d6aa4.xml new file mode 100644 index 0000000..c241478 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_7_4d6aa4.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_10_7_548f7d.xml b/app/src/main/res/drawable/bg_round_corner_10_7_548f7d.xml new file mode 100644 index 0000000..8bacc7c --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_7_548f7d.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_10_7_59548f.xml b/app/src/main/res/drawable/bg_round_corner_10_7_59548f.xml new file mode 100644 index 0000000..546e4dd --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_7_59548f.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_10_7_973a3a.xml b/app/src/main/res/drawable/bg_round_corner_10_7_973a3a.xml new file mode 100644 index 0000000..cdda805 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_7_973a3a.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_10_7_d38c38.xml b/app/src/main/res/drawable/bg_round_corner_10_7_d38c38.xml new file mode 100644 index 0000000..812e9d1 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_7_d38c38.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_10_7_d85e37.xml b/app/src/main/res/drawable/bg_round_corner_10_7_d85e37.xml new file mode 100644 index 0000000..21be1a3 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_7_d85e37.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_13_3_303030.xml b/app/src/main/res/drawable/bg_round_corner_13_3_303030.xml new file mode 100644 index 0000000..9882cce --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_13_3_303030.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_26_7_19ffffff.xml b/app/src/main/res/drawable/bg_round_corner_26_7_19ffffff.xml new file mode 100644 index 0000000..d1bd92f --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_26_7_19ffffff.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_26_7_26ffffff.xml b/app/src/main/res/drawable/bg_round_corner_26_7_26ffffff.xml new file mode 100644 index 0000000..2d54b74 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_26_7_26ffffff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_2_28312b.xml b/app/src/main/res/drawable/bg_round_corner_2_28312b.xml new file mode 100644 index 0000000..9103ffd --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_2_28312b.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_2_6_222222.xml b/app/src/main/res/drawable/bg_round_corner_2_6_222222.xml new file mode 100644 index 0000000..13e4c9c --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_2_6_222222.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_2_6_26310f.xml b/app/src/main/res/drawable/bg_round_corner_2_6_26310f.xml new file mode 100644 index 0000000..6dfdaa5 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_2_6_26310f.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_2_6_28312b.xml b/app/src/main/res/drawable/bg_round_corner_2_6_28312b.xml new file mode 100644 index 0000000..dfa7504 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_2_6_28312b.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_2_6_30176f.xml b/app/src/main/res/drawable/bg_round_corner_2_6_30176f.xml new file mode 100644 index 0000000..3142a5d --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_2_6_30176f.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_2_6_601d14.xml b/app/src/main/res/drawable/bg_round_corner_2_6_601d14.xml new file mode 100644 index 0000000..25f3441 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_2_6_601d14.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_2_6_660fd4.xml b/app/src/main/res/drawable/bg_round_corner_2_6_660fd4.xml new file mode 100644 index 0000000..02ff360 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_2_6_660fd4.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_2_6_b1ef2c.xml b/app/src/main/res/drawable/bg_round_corner_2_6_b1ef2c.xml new file mode 100644 index 0000000..f31ff82 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_2_6_b1ef2c.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_44_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_44_9970ff.xml new file mode 100644 index 0000000..dfe5ec8 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_44_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_000000.xml b/app/src/main/res/drawable/bg_round_corner_5_3_000000.xml new file mode 100644 index 0000000..9214aa1 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_5_3_000000.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_19ffffff.xml b/app/src/main/res/drawable/bg_round_corner_5_3_19ffffff.xml new file mode 100644 index 0000000..6407254 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_5_3_19ffffff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_339970ff_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_5_3_339970ff_9970ff.xml new file mode 100644 index 0000000..8799cad --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_5_3_339970ff_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_e51e0e45_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_5_3_e51e0e45_9970ff.xml new file mode 100644 index 0000000..4a1beea --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_5_3_e51e0e45_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_1f1734_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_6_7_1f1734_9970ff.xml new file mode 100644 index 0000000..10e0b7b --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_1f1734_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_333333_979797.xml b/app/src/main/res/drawable/bg_round_corner_6_7_333333_979797.xml new file mode 100644 index 0000000..cf93ec5 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_333333_979797.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_top_round_corner_8_222222.xml b/app/src/main/res/drawable/bg_top_round_corner_8_222222.xml new file mode 100644 index 0000000..3192647 --- /dev/null +++ b/app/src/main/res/drawable/bg_top_round_corner_8_222222.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_audio_content.xml b/app/src/main/res/layout/activity_audio_content.xml new file mode 100644 index 0000000..287c5c5 --- /dev/null +++ b/app/src/main/res/layout/activity_audio_content.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_audio_content_detail.xml b/app/src/main/res/layout/activity_audio_content_detail.xml new file mode 100644 index 0000000..dbbcdbc --- /dev/null +++ b/app/src/main/res/layout/activity_audio_content_detail.xml @@ -0,0 +1,702 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_audio_content_modify.xml b/app/src/main/res/layout/activity_audio_content_modify.xml new file mode 100644 index 0000000..fbeaf33 --- /dev/null +++ b/app/src/main/res/layout/activity_audio_content_modify.xml @@ -0,0 +1,393 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_audio_content_order_list.xml b/app/src/main/res/layout/activity_audio_content_order_list.xml new file mode 100644 index 0000000..951e278 --- /dev/null +++ b/app/src/main/res/layout/activity_audio_content_order_list.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/app/src/main/res/layout/activity_audio_content_upload.xml b/app/src/main/res/layout/activity_audio_content_upload.xml new file mode 100644 index 0000000..847fccf --- /dev/null +++ b/app/src/main/res/layout/activity_audio_content_upload.xml @@ -0,0 +1,667 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_audio_content_comment.xml b/app/src/main/res/layout/dialog_audio_content_comment.xml new file mode 100644 index 0000000..177474f --- /dev/null +++ b/app/src/main/res/layout/dialog_audio_content_comment.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/dialog_audio_content_delete.xml b/app/src/main/res/layout/dialog_audio_content_delete.xml new file mode 100644 index 0000000..797cd0b --- /dev/null +++ b/app/src/main/res/layout/dialog_audio_content_delete.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_audio_content_order_confirm.xml b/app/src/main/res/layout/dialog_audio_content_order_confirm.xml new file mode 100644 index 0000000..2d9fe05 --- /dev/null +++ b/app/src/main/res/layout/dialog_audio_content_order_confirm.xml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_audio_content_report.xml b/app/src/main/res/layout/dialog_audio_content_report.xml new file mode 100644 index 0000000..ff26f07 --- /dev/null +++ b/app/src/main/res/layout/dialog_audio_content_report.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_audio_content_comment_list.xml b/app/src/main/res/layout/fragment_audio_content_comment_list.xml new file mode 100644 index 0000000..71cdc1a --- /dev/null +++ b/app/src/main/res/layout/fragment_audio_content_comment_list.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_audio_content_comment_reply.xml b/app/src/main/res/layout/fragment_audio_content_comment_reply.xml new file mode 100644 index 0000000..ff53054 --- /dev/null +++ b/app/src/main/res/layout/fragment_audio_content_comment_reply.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_audio_content_main.xml b/app/src/main/res/layout/fragment_audio_content_main.xml new file mode 100644 index 0000000..571926d --- /dev/null +++ b/app/src/main/res/layout/fragment_audio_content_main.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_audio_content_order.xml b/app/src/main/res/layout/fragment_audio_content_order.xml new file mode 100644 index 0000000..7bb9a45 --- /dev/null +++ b/app/src/main/res/layout/fragment_audio_content_order.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_audio_content_theme.xml b/app/src/main/res/layout/fragment_audio_content_theme.xml new file mode 100644 index 0000000..da229ff --- /dev/null +++ b/app/src/main/res/layout/fragment_audio_content_theme.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_audio_content.xml b/app/src/main/res/layout/item_audio_content.xml new file mode 100644 index 0000000..055ff14 --- /dev/null +++ b/app/src/main/res/layout/item_audio_content.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_audio_content_comment.xml b/app/src/main/res/layout/item_audio_content_comment.xml new file mode 100644 index 0000000..7c4a859 --- /dev/null +++ b/app/src/main/res/layout/item_audio_content_comment.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_audio_content_comment_reply.xml b/app/src/main/res/layout/item_audio_content_comment_reply.xml new file mode 100644 index 0000000..8cc07c4 --- /dev/null +++ b/app/src/main/res/layout/item_audio_content_comment_reply.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_audio_content_main.xml b/app/src/main/res/layout/item_audio_content_main.xml new file mode 100644 index 0000000..ef80f0e --- /dev/null +++ b/app/src/main/res/layout/item_audio_content_main.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_audio_content_main_curation.xml b/app/src/main/res/layout/item_audio_content_main_curation.xml new file mode 100644 index 0000000..d997758 --- /dev/null +++ b/app/src/main/res/layout/item_audio_content_main_curation.xml @@ -0,0 +1,46 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/item_audio_content_main_new_content_creator.xml b/app/src/main/res/layout/item_audio_content_main_new_content_creator.xml new file mode 100644 index 0000000..8f8694c --- /dev/null +++ b/app/src/main/res/layout/item_audio_content_main_new_content_creator.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_audio_content_main_new_content_theme.xml b/app/src/main/res/layout/item_audio_content_main_new_content_theme.xml new file mode 100644 index 0000000..b102dc9 --- /dev/null +++ b/app/src/main/res/layout/item_audio_content_main_new_content_theme.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/layout/item_audio_content_order_list.xml b/app/src/main/res/layout/item_audio_content_order_list.xml new file mode 100644 index 0000000..ee20c5f --- /dev/null +++ b/app/src/main/res/layout/item_audio_content_order_list.xml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_audio_content_theme.xml b/app/src/main/res/layout/item_audio_content_theme.xml new file mode 100644 index 0000000..10de4b2 --- /dev/null +++ b/app/src/main/res/layout/item_audio_content_theme.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_audio_other_content.xml b/app/src/main/res/layout/item_audio_other_content.xml new file mode 100644 index 0000000..a766fe5 --- /dev/null +++ b/app/src/main/res/layout/item_audio_other_content.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f23a871..71929ee 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -70,4 +70,28 @@ #4C9970FF #4DD8D8D8 #3E737C + #161616 + #88E2E2E2 + #3BAC6A + #28312B + #595959 + #973A3A + #D85E37 + #D38C38 + #59548F + #4D6AA4 + #2D7390 + #548F7D + #CC979797 + #E33621 + #601D14 + #26310F + #B1EF2C + #30176F + #19FFFFFF + #E51E0E45 + #CC000000 + #26FFFFFF + #979797 + #660FD4