Compare commits

...

52 Commits

Author SHA1 Message Date
Yu Sung f444f0bfb0 라이브
- 최대 스피커 수 방장 포함 6명으로 변경
2024-09-20 17:55:20 +09:00
Yu Sung ac304e7d17 크리에이터 채널 - 공유 버튼 변경
라이브 - 큰 음소거 이미지 변경
2024-09-20 17:46:29 +09:00
Yu Sung 3db59e6236 라이브 정보 수정
- 연령 제한 설정 추가
2024-09-11 23:26:17 +09:00
Yu Sung 8aa69f02fc 시리즈 콘텐츠 리스트
- 정렬(최신순, 등록순) 추가
2024-09-10 18:05:05 +09:00
Yu Sung 7c5b30335e 커뮤니티 댓글, 팬토크, 콘텐츠 댓글
- 프로필 이미지 터치시 차단, 신고가 가능한 유저 프로필 표시
2024-09-07 03:58:52 +09:00
Yu Sung 17ead38524 PG 결제 - 카카오페이 결제
- 구매자 정보 추가
2024-09-06 14:47:41 +09:00
Yu Sung bc8d3e6d70 라이브 방 - 유저 차단 팝업
- 리스너가 차단할 때 팝업 문구 수정
2024-09-05 20:02:50 +09:00
Yu Sung 66ce37defa 라이브 방
- 차단한 유저의 채팅이 보이지 않도록 수정
2024-09-05 18:12:10 +09:00
Yu Sung 7408288d85 차단 유저 리스트 페이지 추가 2024-09-04 18:00:11 +09:00
Yu Sung 2bea2365a0 마이 페이지
- 팔로잉 리스트 버튼 추가
2024-09-04 15:43:34 +09:00
Yu Sung f4d95e6755 댓글 입력창
- 우측에 불필요한 빈칸 제거
2024-08-30 18:00:46 +09:00
Yu Sung 30a480232b 비밀후원 체크박스 네모로 변경 2024-08-30 17:57:18 +09:00
Yu Sung 468a876e33 라이브 방
- 후원현황과 후원하기 버튼 순서 변경
2024-08-30 17:53:57 +09:00
Yu Sung 0f1de35e62 콘텐츠 상세 - 비밀댓글 등록
- 비밀댓글 체크박스 이미지 네모로 변경
2024-08-30 17:50:39 +09:00
Yu Sung 24f09d068d 콘텐츠 댓글 리스트
- 비밀댓글은 닉네임 옆에 '비밀댓글' 마크 추가
2024-08-30 17:47:44 +09:00
Yu Sung d1a90ad599 콘텐츠 댓글 리스트
- 유료 콘텐츠를 구매한 사람이 비밀댓글을 등록할 수 있는 기능 추가
2024-08-30 17:24:26 +09:00
Yu Sung fa849dd5b6 콘텐츠 상세
- 댓글이 없을 때 유료 콘텐츠를 구매한 사람이 비밀댓글을 등록할 수 있는 기능 추가
2024-08-30 17:15:13 +09:00
Yu Sung ca9dee5574 캔 충전 - 결제수단 카카오페이 추가 2024-08-30 17:00:05 +09:00
Yu Sung f5445a3c48 스플래시 2024/09 2024-08-27 00:06:10 +09:00
Yu Sung 534a6e737e 라이브 후원
- 비밀 후원 기능 추가
2024-08-26 20:07:37 +09:00
Yu Sung 81846f7f7b 콘텐츠 메인
- 우측 최상단에 콘텐츠 보관함
- 배너와 추천 시리즈 순서 변경
- 내 보관함 제거
2024-08-21 21:49:21 +09:00
Yu Sung 47cd685f80 라이브
- 후원 메시지, 룰렛 결과 모든 유저에게 보이도록 수정
2024-08-21 21:40:14 +09:00
Yu Sung 835ece8a6b 커뮤니티 오디오 녹음
- mode: videoRecording
- 재생 mode: moviePlayback
2024-08-08 03:10:07 +09:00
Yu Sung ef3494dcb1 커뮤니티 오디오 녹음
- 녹음시간이 3분이 되면 녹음 멈춤
2024-08-08 01:54:29 +09:00
Yu Sung 85a871693c 커뮤니티 오디오 녹음
- audioSession.setCategory mode videoRecording으로 수정
2024-08-08 01:34:14 +09:00
Yu Sung 0be8d0d98a 커뮤니티 게시물 등록
- 오디오 녹음 기능 추가
2024-08-07 23:49:52 +09:00
Yu Sung 22ab76d664 크리에이터 커뮤니티 게시글 전체리스트
- 오디오 재생기능 추가
2024-08-07 14:48:12 +09:00
Yu Sung a3beb9c9fe 온보딩 이미지 변경 2024-07-31 23:31:56 +09:00
Yu Sung e41549ea07 회사정보 고객센터 이용시간 추가 2024-07-31 18:31:33 +09:00
Yu Sung 89dc0b4540 앱 가이드 이미지 변경 2024-07-30 22:46:48 +09:00
Yu Sung b17a037b0a 앱 아이콘 변경 2024-07-30 22:17:14 +09:00
Yu Sung c1d63675db 스플래시 변경 2024-07-30 21:34:23 +09:00
Yu Sung 6d83869cae 휴대폰 결제도 헥토파이낸셜로 수정 2024-07-30 19:17:23 +09:00
Yu Sung 6884a060e4 휴대폰 결제가 아닌경우 user설정을 하도록 수정 2024-07-04 16:16:09 +09:00
Yu Sung 313af1949e 룰렛 합계 검증
- Float 값이라 정확히 100.0이 나오지 않으므로 totalPercentage가 99.9보다 크고 100.1보다 작으면 통과되도록 조건 수정
2024-07-02 22:12:05 +09:00
Yu Sung 9f402c8ec8 PG 수정
- 휴대폰 결제: 웰컴페이먼츠
- 나머지 : 헥토파이낸스(세틀뱅크)
2024-07-02 14:50:01 +09:00
Yu Sung 45b600ac41 통신판매업신고번호 추가 2024-06-29 18:35:21 +09:00
Yu Sung e1d42b5495 PG 수정
- 휴대폰 결제: 웰컴페이먼츠
- 나머지 : 헥토파이낸스(세틀뱅크)
2024-06-29 18:34:30 +09:00
Yu Sung 03d86ee2d5 라이브 공유하기 버튼 다시 추가 2024-06-28 13:30:55 +09:00
Yu Sung 9533e97f89 콘텐츠 등록
- 한정판 등록 기능 추가
2024-06-07 14:51:24 +09:00
Yu Sung 5fc0f1789a 콘텐츠 리스트
- 소장중 / 대여중 / Sold Out 표시
2024-06-04 19:32:26 +09:00
Yu Sung 9819f00d0d 크리에이터 커뮤니티 게시물
- 구매하지 않은 유료 게시물도 내용은 보이도록 수정
2024-05-30 13:25:47 +09:00
Yu Sung 27ee83f74b 스플래시 06월 2024-05-28 16:23:42 +09:00
Yu Sung 987c96bee8 커뮤니티 게시물
- 무료이거나 구매한 게시물은 이미지가 블러 처리 되지 않도록 수정
2024-05-27 22:52:35 +09:00
Yu Sung 76c20c2658 심사용 계정이 아닌 곳에서 가격 * 110으로 표시되는 버그 수정 2024-05-27 22:18:33 +09:00
Yu Sung db828cf44d 커뮤니티 게시글 구매 알림창
- 구매버튼 캔 이미지 제거
2024-05-25 00:12:59 +09:00
Yu Sung ee1664cf19 회사정보
- 대표 이메일 추가
2024-05-25 00:04:45 +09:00
Yu Sung 0a1b1865dd 콘텐츠 메인 탭
- 하단에 회사정보 추가
2024-05-24 23:38:04 +09:00
Yu Sung e7cbabb285 크리에이터 커뮤니티
- 텍스트뷰의 width를 screenSize().width - 42로 설정하여 아이템에 horizontal padding이 적용되지 않던 버그 수정
2024-05-24 22:28:35 +09:00
Yu Sung 3ae5ea776c 커뮤니티 유료 게시글 조회, 구매 기능 추가 2024-05-24 16:19:43 +09:00
Yu Sung 0a96509b35 커뮤니티 게시글 등록
- 유료 게시글 등록을 위해 가격 설정 추가
2024-05-24 15:11:40 +09:00
Yu Sung 2268b0c1bc 마이페이지 탭
- PG심사용 계정의 경우 캔 표시 영역 뷰 제거
2024-05-24 00:19:35 +09:00
123 changed files with 3194 additions and 622 deletions

View File

@ -6,7 +6,7 @@ target 'SodaLive' do
use_frameworks!
# Pods for SodaLive
pod 'BootpayUI', '4.3.0'
pod 'BootpayUI', '4.4.0'
pod 'ObjectBox'
end
@ -16,7 +16,7 @@ target 'SodaLive-dev' do
use_frameworks!
# Pods for SodaLive-dev
pod 'BootpayUI', '4.3.0'
pod 'BootpayUI', '4.4.0'
pod 'ObjectBox'
end

View File

@ -1,27 +1,31 @@
PODS:
- Alamofire (5.7.1)
- Bootpay (4.2.9):
- Alamofire (5.9.1)
- Bootpay (4.4.4):
- CryptoSwift
- NVActivityIndicatorView
- ObjectMapper
- BootpayUI (4.3.0):
- BootpayUI (4.4.0):
- Alamofire
- Bootpay (~> 4.2.8)
- Bootpay (~> 4.4.0)
- CryptoSwift
- JGProgressHUD
- ObjectMapper
- SCLAlertView
- SnapKit
- SwiftyJSON
- CryptoSwift (1.7.1)
- CryptoSwift (1.8.3)
- JGProgressHUD (2.2)
- NVActivityIndicatorView (5.2.0):
- NVActivityIndicatorView/Base (= 5.2.0)
- NVActivityIndicatorView/Base (5.2.0)
- ObjectBox (1.8.1)
- ObjectMapper (4.2.0)
- ObjectMapper (4.4.2)
- SCLAlertView (0.8)
- SnapKit (5.6.0)
- SwiftyJSON (5.0.1)
- SnapKit (5.7.1)
- SwiftyJSON (5.0.2)
DEPENDENCIES:
- BootpayUI (= 4.3.0)
- BootpayUI (= 4.4.0)
- ObjectBox
SPEC REPOS:
@ -31,6 +35,7 @@ SPEC REPOS:
- BootpayUI
- CryptoSwift
- JGProgressHUD
- NVActivityIndicatorView
- ObjectBox
- ObjectMapper
- SCLAlertView
@ -38,17 +43,18 @@ SPEC REPOS:
- SwiftyJSON
SPEC CHECKSUMS:
Alamofire: 0123a34370cb170936ae79a8df46cc62b2edeb88
Bootpay: d753088334a16ce99094142beb66a6610a15d84b
BootpayUI: 54dcbe59a23e0d91b07a8add8115e1a6deace0f0
CryptoSwift: d3d18dc357932f7e6d580689e065cf1f176007c1
Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c
Bootpay: ed9b04d0061931d4bb0c6a2e14dc44222168fde6
BootpayUI: 58e4c9a23ffb65b8023ef9f3dcb1d70090599e69
CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483
JGProgressHUD: d83d7a981b85d11205e19ff8ad5bb9c40571c847
NVActivityIndicatorView: fe52a6a68664c2df8991d7d9e3d86d8d19453c53
ObjectBox: a7900d5335218cd437cbc080b7ccc38a5211f7b4
ObjectMapper: 1eb41f610210777375fa806bf161dc39fb832b81
ObjectMapper: e6e4d91ff7f2861df7aecc536c92d8363f4c9677
SCLAlertView: 6a77bb2edfc65e04dbe57725546cb4107a506b85
SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25
SwiftyJSON: 2f33a42c6fbc52764d96f13368585094bfd8aa5e
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
SwiftyJSON: f5b1bf1cd8dd53cd25887ac0eabcfd92301c6a5a
PODFILE CHECKSUM: cdff30c96e85662f4de75ddd8d54358311c1e629
PODFILE CHECKSUM: 48980f586cd82e7704768a64fd3ca78a3c3cb0c6
COCOAPODS: 1.14.3
COCOAPODS: 1.15.2

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "launcher_icon_1024px.png",
"filename" : "launcher_1024x1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "btn_square_select_checked.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "btn_square_select_normal.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_content_keep.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

View File

@ -9,7 +9,7 @@
"scale" : "2x"
},
{
"filename" : "splash_text_2.png",
"filename" : "ic_kakaopay.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_lock_bb.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -9,7 +9,7 @@
"scale" : "2x"
},
{
"filename" : "splash_bg.png",
"filename" : "splash_bg.jpg",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "splash_title.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -4,11 +4,212 @@
<dict>
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-1299501215847962~8852459715</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>SKAdNetworkItems</key>
<array>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cstr6suwn9.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4fzdc2evr5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4pfyvq9l8r.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>2fnua5tdw4.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ydx93a7ass.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>5a6flpkh64.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>p78axxw29g.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v72qych5uu.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ludvb6z3bs.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cp8zw746q7.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3sh42y64q3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>c6k4g5qg8m.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>s39g8k73mm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3qy4746246.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>f38h382jlk.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>hs6bdukanm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v4nxqhlyqp.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>wzmmz9fp6w.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>yclnxrl5pm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>t38b2kh725.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>7ug5zh24hu.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>gta9lk7p23.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>vutu7akeur.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>y5ghdn5j9k.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>n6fk4nfna4.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v9wttpbfk9.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>n38lu8286q.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>47vhws6wlr.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>kbd757ywx3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>9t245vhmpl.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>eh6m2bh4zr.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>a2p9lx4jpn.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>22mmun2rn5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4468km3ulz.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>2u9pt9hc89.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>8s468mfl3y.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>klf5c3l5u5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ppxm28t8ap.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ecpz2srf59.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>uw77j35x4d.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>pwa73g5rt2.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>mlmmfzh3r3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>578prtvx9j.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4dzt52r2t5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>e5fvkxwrpn.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>8c4e2ghe7u.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>zq492l623r.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3rd42ekr43.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3qcr597p9d.skadnetwork</string>
</dict>
</array>
<key>UIAppFonts</key>
<array>
<string>gmarket_sans_bold.otf</string>
@ -21,206 +222,5 @@
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-1299501215847962~8852459715</string>
<key>SKAdNetworkItems</key>
<array>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cstr6suwn9.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4fzdc2evr5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4pfyvq9l8r.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>2fnua5tdw4.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ydx93a7ass.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>5a6flpkh64.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>p78axxw29g.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v72qych5uu.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ludvb6z3bs.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cp8zw746q7.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3sh42y64q3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>c6k4g5qg8m.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>s39g8k73mm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3qy4746246.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>f38h382jlk.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>hs6bdukanm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v4nxqhlyqp.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>wzmmz9fp6w.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>yclnxrl5pm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>t38b2kh725.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>7ug5zh24hu.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>gta9lk7p23.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>vutu7akeur.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>y5ghdn5j9k.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>n6fk4nfna4.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v9wttpbfk9.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>n38lu8286q.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>47vhws6wlr.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>kbd757ywx3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>9t245vhmpl.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>eh6m2bh4zr.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>a2p9lx4jpn.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>22mmun2rn5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4468km3ulz.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>2u9pt9hc89.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>8s468mfl3y.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>klf5c3l5u5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ppxm28t8ap.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ecpz2srf59.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>uw77j35x4d.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>pwa73g5rt2.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>mlmmfzh3r3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>578prtvx9j.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4dzt52r2t5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>e5fvkxwrpn.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>8c4e2ghe7u.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>zq492l623r.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3rd42ekr43.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3qcr597p9d.skadnetwork</string>
</dict>
</array>
</dict>
</plist>

View File

@ -111,6 +111,23 @@ final class Agora {
rtmKit?.send(message, toPeer: peerId, completion: completion)
}
func sendRawMessageToPeer(peerId: String, rawMessage: LiveRoomChatRawMessage, completion: AgoraRtmSendPeerMessageBlock? = nil, fail: (() -> Void)? = nil) {
let encoder = JSONEncoder()
let jsonMessageData = try? encoder.encode(rawMessage)
let option = AgoraRtmSendMessageOptions()
option.enableOfflineMessaging = false
option.enableHistoricalMessaging = false
if let jsonMessageData = jsonMessageData {
let message = AgoraRtmRawMessage(rawData: jsonMessageData, description: "")
rtmKit?.send(message, toPeer: peerId, sendMessageOptions: option, completion: completion)
} else {
if let fail = fail {
fail()
}
}
}
func mute(_ isMute: Bool) {
rtcEngine?.muteLocalAudioStream(isMute)
}

View File

@ -129,4 +129,6 @@ enum AppStep {
case seriesContentAll(seriesId: Int, seriesTitle: String)
case tempCanPayment(orderType: OrderType, contentId: Int, title: String, can: Int)
case blockList
}

View File

@ -57,6 +57,9 @@ struct SodaLiveApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
CreatorCommunityMediaPlayerManager.shared.pauseContent()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
UIApplication.shared.applicationIconBadgeNumber = 0

View File

@ -91,7 +91,34 @@ struct ContentListItemView: View {
Spacer()
if item.price > 0 {
if item.isOwned {
Text("소장중")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color.gray11)
.padding(.horizontal, 5.3)
.padding(.vertical, 2.7)
.background(Color(hex: "b1ef2c"))
.cornerRadius(2.6)
} else if item.isRented {
Text("대여중")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color.white)
.padding(.horizontal, 5.3)
.padding(.vertical, 2.7)
.background(Color(hex: "660fd4"))
.cornerRadius(2.6)
} else if item.isSoldOut {
Text("Sold Out")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color.grayd2)
.padding(.horizontal, 5.3)
.padding(.vertical, 2.7)
.overlay(
RoundedRectangle(cornerRadius: 2.6)
.stroke(Color.grayd2, lineWidth: 1)
)
.cornerRadius(2.6)
} else if item.price > 0 {
HStack(spacing: 8) {
Image("ic_can")
.resizable()
@ -130,7 +157,10 @@ struct ContentListItemView_Previews: PreviewProvider {
commentCount: 0,
isPin: true,
isAdult: false,
isScheduledToOpen: true
isScheduledToOpen: true,
isRented: false,
isOwned: false,
isSoldOut: true
)
)
}

View File

@ -87,7 +87,7 @@ extension ContentPlayManager {
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default)
try audioSession.setCategory(.playback, mode: .moviePlayback)
try audioSession.setActive(true)
self.player = try AVAudioPlayer(data: audioData)

View File

@ -26,8 +26,8 @@ final class ContentRepository {
return api.requestPublisher(.likeContent(request: PutAudioContentLikeRequest(contentId: audioContentId)))
}
func registerComment(audioContentId: Int, comment: String, parentId: Int? = nil) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.registerComment(request: RegisterAudioContentCommentRequest(comment: comment, contentId: audioContentId, parentId: parentId)))
func registerComment(audioContentId: Int, comment: String, parentId: Int? = nil, isSecret: Bool = false) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.registerComment(request: RegisterAudioContentCommentRequest(comment: comment, contentId: audioContentId, parentId: parentId, isSecret: isSecret)))
}
func orderAudioContent(contentId: Int, orderType: OrderType) -> AnyPublisher<Response, MoyaError> {

View File

@ -219,7 +219,7 @@ struct ContentCreateView: View {
VStack(spacing: 13.3) {
Text("소장 설정")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) {
@ -241,7 +241,7 @@ struct ContentCreateView: View {
VStack(spacing: 0) {
Text(viewModel.isOnlyRental ? "대여 가격" : "소장 가격")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "d2d2d2"))
.foregroundColor(Color.grayd2)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 0) {
@ -249,7 +249,7 @@ struct ContentCreateView: View {
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
.cornerRadius(6.7)
.keyboardType(.numberPad)
.padding(.trailing, 10)
@ -258,16 +258,16 @@ struct ContentCreateView: View {
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
}
.padding(.vertical, 17)
.padding(.horizontal, 13.3)
.background(Color(hex: "222222"))
.background(Color.gray22)
.cornerRadius(5.3)
.padding(.top, 5.3)
Rectangle()
.foregroundColor(Color(hex: "232323"))
.foregroundColor(Color.gray23)
.frame(height: 1)
.padding(.top, 11)
@ -289,6 +289,44 @@ struct ContentCreateView: View {
}
.padding(.top, 26.7)
if viewModel.price > 0 && !viewModel.isOnlyRental {
VStack(spacing: 13.3) {
Text("한정판 설정")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color.grayee)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) {
SelectButtonView(title: "무제한", isChecked: !viewModel.isLimited) {
if viewModel.isLimited {
viewModel.isLimited = false
}
}
SelectButtonView(title: "한정판", isChecked: viewModel.isLimited) {
if !viewModel.isLimited {
viewModel.isLimited = true
}
}
}
if viewModel.isLimited {
TextField("한정판 개수를 입력하세요", text: $viewModel.limitedString)
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color.grayee)
.cornerRadius(6.7)
.keyboardType(.numberPad)
.padding(.vertical, 17)
.padding(.horizontal, 13.3)
.background(Color.gray22)
.cornerRadius(5.3)
}
}
.padding(.top, 26.7)
}
VStack(spacing: 13.3) {
Text("미리듣기")
.font(.custom(Font.bold.rawValue, size: 16.7))

View File

@ -55,15 +55,48 @@ final class ContentCreateViewModel: ObservableObject {
didSet {
if isFree {
priceString = "0"
isLimited = false
isOnlyRental = false
isGeneratePreview = true
}
}
}
@Published var isOnlyRental = false
@Published var isOnlyRental = false {
didSet {
if isOnlyRental {
isLimited = false
}
}
}
@Published var isGeneratePreview = true
@Published var isLimited = false {
didSet {
if !isLimited {
limitedString = ""
}
}
}
@Published var limited: Int? = nil {
didSet {
if let limited = limited, limited <= 0 {
limitedString = ""
}
}
}
@Published var limitedString: String = "" {
didSet {
if limitedString == "" {
limited = nil
} else {
limited = Int(limitedString)
}
}
}
@Published var previewStartTime: String = ""
@Published var previewEndTime: String = ""
@Published var releaseDateString: String = Date().convertDateFormat(dateFormat: "yyyy.MM.dd")
@ -92,6 +125,7 @@ final class ContentCreateViewModel: ObservableObject {
detail: detail,
tags: hashtags,
price: price,
limited: limited,
releaseDate: isActiveReservation ? "\(releaseDate.convertDateFormat(dateFormat: "yyyy-MM-dd")) \(releaseTime.convertDateFormat(dateFormat: "HH:mm"))" : nil,
timezone: TimeZone.current.identifier,
themeId: theme!.id,

View File

@ -12,6 +12,7 @@ struct CreateAudioContentRequest: Encodable {
let detail: String
let tags: String
let price: Int
let limited: Int?
let releaseDate: String?
let timezone: String
let themeId: Int

View File

@ -17,6 +17,7 @@ struct AudioContentCommentItemView: View {
let modifyComment: (Int, String) -> Void
let onClickDelete: (Int) -> Void
let onClickProfile: (Int) -> Void
@State var isShowPopupMenu: Bool = false
@State var isModeModify: Bool = false
@ -30,15 +31,32 @@ struct AudioContentCommentItemView: View {
.resizable()
.frame(width: 40, height: 40)
.clipShape(Circle())
.onTapGesture {
if UserDefaults.int(forKey: .userId) != commentItem.writerId {
onClickProfile(commentItem.writerId)
}
}
VStack(alignment: .leading, spacing: 0) {
Text(commentItem.nickname)
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color.gray90)
HStack(spacing: 6.7) {
Text(commentItem.nickname)
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color.gray90)
if commentItem.isSecret {
Text("비밀댓글")
.font(.custom(Font.medium.rawValue, size: 11))
.foregroundColor(Color.grayee)
.padding(.horizontal, 4)
.padding(.vertical, 2)
.background(Color.button.opacity(0.2))
.cornerRadius(3.3)
}
}
Text(commentItem.date)
.font(.custom(Font.medium.rawValue, size: 10.3))
.foregroundColor(Color(hex: "525252"))
.foregroundColor(Color.gray52)
.padding(.top, 4)
}
@ -83,8 +101,8 @@ struct AudioContentCommentItemView: View {
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.accentColor(Color(hex: "3bb9f1"))
.foregroundColor(Color.grayee)
.accentColor(Color.button)
.keyboardType(.default)
.padding(.horizontal, 13.3)
@ -102,7 +120,7 @@ struct AudioContentCommentItemView: View {
isModeModify = false
}
}
.background(Color(hex: "232323"))
.background(Color.gray23)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
@ -139,7 +157,7 @@ struct AudioContentCommentItemView: View {
.padding(.leading, 46.7)
Rectangle()
.foregroundColor(Color(hex: "595959"))
.foregroundColor(Color.gray59)
.frame(height: 0.5)
.padding(.top, 16.7)
}
@ -149,7 +167,7 @@ struct AudioContentCommentItemView: View {
if commentItem.writerId == UserDefaults.int(forKey: .userId) {
Text("수정")
.font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
.onTapGesture {
isModeModify = true
isShowPopupMenu = false
@ -161,7 +179,7 @@ struct AudioContentCommentItemView: View {
{
Text("삭제")
.font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
.onTapGesture {
onClickDelete(commentItem.id)
isShowPopupMenu = false
@ -169,7 +187,7 @@ struct AudioContentCommentItemView: View {
}
}
.padding(10)
.background(Color(hex: "222222"))
.background(Color.gray22)
}
}
.onAppear { comment = commentItem.comment }

View File

@ -14,12 +14,16 @@ struct AudioContentCommentListView: View {
let creatorId: Int
let audioContentId: Int
let isShowSecret: Bool
@StateObject var viewModel = AudioContentCommentListViewModel()
@State private var commentId: Int = 0
@State private var isShowDeletePopup: Bool = false
@State private var memberId: Int = 0
@State private var isShowMemberProfilePopup: Bool = false
var body: some View {
NavigationView {
ZStack {
@ -50,6 +54,28 @@ struct AudioContentCommentListView: View {
.padding(.bottom, 13.3)
.padding(.horizontal, 13.3)
if isShowSecret {
HStack(spacing: 8) {
Spacer()
Image(viewModel.isSecret ? "btn_square_select_checked" : "btn_square_select_normal")
.resizable()
.frame(width: 20, height: 20)
.onTapGesture {
viewModel.isSecret.toggle()
}
Text("비밀댓글")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(viewModel.isSecret ? Color.button : Color.grayee)
.onTapGesture {
viewModel.isSecret.toggle()
}
}
.padding(.bottom, 13.3)
.padding(.horizontal, 13.3)
}
HStack(spacing: 8) {
KFImage(URL(string: UserDefaults.string(forKey: .profileImage)))
.cancelOnDisappear(true)
@ -63,8 +89,8 @@ struct AudioContentCommentListView: View {
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.accentColor(Color(hex: "3bb9f1"))
.foregroundColor(Color.grayee)
.accentColor(Color.button)
.keyboardType(.default)
.padding(.horizontal, 13.3)
@ -79,20 +105,18 @@ struct AudioContentCommentListView: View {
viewModel.registerComment()
}
}
.background(Color(hex: "232323"))
.background(Color.gray23)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "3bb9f1"))
.foregroundColor(Color.button)
)
Spacer()
}
.padding(.horizontal, 13.3)
Rectangle()
.foregroundColor(Color(hex: "595959"))
.foregroundColor(Color.gray59)
.frame(height: 0.5)
.padding(.top, 12)
.padding(.bottom, 13.3)
@ -116,6 +140,10 @@ struct AudioContentCommentListView: View {
onClickDelete: {
commentId = $0
isShowDeletePopup = true
},
onClickProfile: {
memberId = $0
isShowMemberProfilePopup = true
}
)
.padding(.horizontal, 26.7)
@ -148,6 +176,10 @@ struct AudioContentCommentListView: View {
)
}
if isShowMemberProfilePopup {
MemberProfileDialog(isShowing: $isShowMemberProfilePopup, memberId: memberId)
}
if viewModel.isLoading {
LoadingView()
}

View File

@ -22,6 +22,8 @@ class AudioContentCommentListViewModel: ObservableObject {
@Published var totalCommentCount = 0
@Published var commentList = [GetAudioContentCommentListItem]()
@Published var isSecret = false
var audioContentId = 0
var page = 1
var isLast = false
@ -84,7 +86,7 @@ class AudioContentCommentListViewModel: ObservableObject {
isLoading = true
repository.registerComment(audioContentId: audioContentId, comment: comment)
repository.registerComment(audioContentId: audioContentId, comment: comment, isSecret: isSecret)
.sink { result in
switch result {
case .finished:

View File

@ -20,6 +20,9 @@ struct AudioContentListReplyView: View {
@State private var commentId: Int = 0
@State private var isShowDeletePopup: Bool = false
@State private var memberId: Int = 0
@State private var isShowMemberProfilePopup: Bool = false
var body: some View {
ZStack {
VStack(spacing: 0) {
@ -98,7 +101,11 @@ struct AudioContentListReplyView: View {
isReplyComment: true,
isShowPopupMenuButton: false,
modifyComment: { _, _ in },
onClickDelete: { _ in }
onClickDelete: { _ in },
onClickProfile: {
memberId = $0
isShowMemberProfilePopup = true
}
)
.padding(.horizontal, 26.7)
.padding(.bottom, 13.3)
@ -120,6 +127,10 @@ struct AudioContentListReplyView: View {
onClickDelete: {
commentId = $0
isShowDeletePopup = true
},
onClickProfile: {
memberId = $0
isShowMemberProfilePopup = true
}
)
.padding(.horizontal, 40)
@ -153,6 +164,10 @@ struct AudioContentListReplyView: View {
)
}
if isShowMemberProfilePopup {
MemberProfileDialog(isShowing: $isShowMemberProfilePopup, memberId: memberId)
}
if viewModel.isLoading {
LoadingView()
}

View File

@ -12,10 +12,12 @@ struct ContentDetailCommentView: View {
let commentCount: Int
let commentList: [GetAudioContentCommentListItem]
let isShowSecret: Bool
let registerComment: (String) -> Void
let registerComment: (String, Bool) -> Void
@State private var comment = ""
@State private var isSecret = false
var body: some View {
VStack(alignment: .leading, spacing: 10.3) {
@ -26,9 +28,24 @@ struct ContentDetailCommentView: View {
Text("\(commentCount)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "909090"))
.foregroundColor(Color.gray90)
Spacer()
if isShowSecret && commentCount <= 0 {
HStack(spacing: 8) {
Image(isSecret ? "btn_square_select_checked" : "btn_square_select_normal")
.resizable()
.frame(width: 20, height: 20)
Text("비밀댓글")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(isSecret ? Color.button : Color.grayee)
}
.onTapGesture {
isSecret.toggle()
}
}
}
HStack(spacing: 8) {
@ -48,7 +65,7 @@ struct ContentDetailCommentView: View {
if commentCount > 0 {
Text(commentList[0].comment)
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "bbbbbb"))
.foregroundColor(Color.graybb)
.lineLimit(1)
.lineSpacing(8)
.padding(.leading, 3)
@ -58,8 +75,8 @@ struct ContentDetailCommentView: View {
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.accentColor(Color(hex: "3bb9f1"))
.foregroundColor(Color.grayee)
.accentColor(Color.button)
.keyboardType(.default)
.padding(.horizontal, 13.3)
@ -71,15 +88,15 @@ struct ContentDetailCommentView: View {
.padding(6.7)
.onTapGesture {
hideKeyboard()
registerComment(comment)
registerComment(comment, isSecret)
}
}
.background(Color(hex: "232323"))
.background(Color.gray23)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "3bb9f1"))
.foregroundColor(Color.button)
)
}

View File

@ -18,6 +18,7 @@ struct GetAudioContentCommentListItem: Decodable {
let nickname: String
let profileUrl: String
let comment: String
let isSecret: Bool
let donationCan: Int
let date: String
let replyCount: Int

View File

@ -11,4 +11,5 @@ struct RegisterAudioContentCommentRequest: Encodable {
let comment: String
let contentId: Int
let parentId: Int?
let isSecret: Bool
}

View File

@ -136,8 +136,9 @@ struct ContentDetailView: View {
ContentDetailCommentView(
commentCount: audioContent.commentCount,
commentList: audioContent.commentList,
registerComment: { comment in
self.viewModel.registerComment(comment: comment)
isShowSecret: audioContent.existOrdered,
registerComment: { comment, isSecret in
self.viewModel.registerComment(comment: comment, isSecret: isSecret)
}
)
.padding(10.3)
@ -233,7 +234,7 @@ struct ContentDetailView: View {
orderType: orderType,
contentId: audioContent.contentId,
title: audioContent.title,
can: orderType == .RENTAL ? Int(ceil(Double(audioContent.price) * 0.6)) : audioContent.price
can: !audioContent.isOnlyRental && orderType == .RENTAL ? Int(ceil(Double(audioContent.price) * 0.6)) : audioContent.price
)
)
} else {
@ -323,7 +324,7 @@ struct ContentDetailView: View {
}
if viewModel.isShowDonationPopup {
LiveRoomDonationDialogView(isShowing: $viewModel.isShowDonationPopup, isAudioContentDonation: true) { can, comment in
LiveRoomDonationDialogView(isShowing: $viewModel.isShowDonationPopup, isAudioContentDonation: true) { can, comment, _ in
viewModel.donation(can: can, comment: comment)
}
}
@ -360,7 +361,8 @@ struct ContentDetailView: View {
AudioContentCommentListView(
isPresented: $isShowCommentListView,
creatorId: viewModel.audioContent!.creator.creatorId,
audioContentId: viewModel.audioContent!.contentId
audioContentId: viewModel.audioContent!.contentId,
isShowSecret: viewModel.audioContent!.existOrdered
)
}
)

View File

@ -239,14 +239,14 @@ final class ContentDetailViewModel: ObservableObject {
}
}
func registerComment(comment: String) {
func registerComment(comment: String, isSecret: Bool) {
if comment.trimmingCharacters(in: .whitespaces).isEmpty {
return
}
isLoading = true
repository.registerComment(audioContentId: contentId, comment: comment)
repository.registerComment(audioContentId: contentId, comment: comment, isSecret: isSecret)
.sink { result in
switch result {
case .finished:

View File

@ -46,9 +46,15 @@ struct ContentOrderDialogView: View {
.frame(width: 16.7, height: 16.7)
}
Text(isOnlyRental ? "\(price * 110)" : "\(Int(ceil(Double(price) * 0.6)) * 110)")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
if UserDefaults.int(forKey: .userId) == 17958 {
Text(isOnlyRental ? "\(price * 110)" : "\(Int(ceil(Double(price) * 0.6)) * 110)")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
} else {
Text(isOnlyRental ? "\(price)" : "\(Int(ceil(Double(price) * 0.6)))")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
}
if UserDefaults.int(forKey: .userId) == 17958 {
Text("")
@ -87,9 +93,15 @@ struct ContentOrderDialogView: View {
.frame(width: 16.7, height: 16.7)
}
Text("\(price * 110)")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
if UserDefaults.int(forKey: .userId) == 17958 {
Text("\(price * 110)")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
} else {
Text("\(price)")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
}
if UserDefaults.int(forKey: .userId) == 17958 {
Text("")

View File

@ -18,10 +18,11 @@ struct LiveRoomDonationDialogView: View {
@State private var donationMessage = ""
@State private var isShowErrorPopup = false
@State private var errorMessage = ""
@State private var isSecret = false
@Binding var isShowing: Bool
let isAudioContentDonation: Bool
let onClickDonation: (Int, String) -> Void
let onClickDonation: (Int, String, Bool) -> Void
@StateObject var keyboardHandler = KeyboardHandler()
@ -82,6 +83,27 @@ struct LiveRoomDonationDialogView: View {
.foregroundColor(Color.gray90)
.padding(.top, 16)
if !isAudioContentDonation {
HStack(spacing: 0) {
Spacer()
HStack(spacing: 8) {
Image(isSecret ? "btn_square_select_checked" : "btn_square_select_normal")
.resizable()
.frame(width: 20, height: 20)
Text("비밀후원")
.font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(isSecret ? Color.button : Color.grayee)
}
.onTapGesture {
isSecret.toggle()
}
}
.padding(.horizontal, 20)
.padding(.top, 16)
}
TextField("몇 캔을 후원할까요?", text: $donationCan)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
@ -221,7 +243,7 @@ struct LiveRoomDonationDialogView: View {
.onTapGesture {
if !donationCan.trimmingCharacters(in: .whitespaces).isEmpty,
let can = Int(donationCan) {
onClickDonation(can, donationMessage)
onClickDonation(can, donationMessage, isSecret)
isShowing = false
} else {
errorMessage = "1캔 이상 후원하실 수 있습니다."

View File

@ -17,19 +17,28 @@ struct ContentMainView: View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: 0) {
Text("콘텐츠 마켓")
.font(.custom(Font.bold.rawValue, size: 21.3))
.foregroundColor(Color(hex: "3bb9f1"))
.padding(.bottom, 26.7)
.padding(.horizontal, 13.3)
HStack(spacing: 0) {
Text("콘텐츠 마켓")
.font(.custom(Font.bold.rawValue, size: 21.3))
.foregroundColor(Color.button)
Spacer()
Image("ic_content_keep")
.onTapGesture {
AppState.shared.setAppStep(step: .orderListAll)
}
}
.padding(.bottom, 26.7)
.padding(.horizontal, 13.3)
if !viewModel.isLoading {
ContentMainRecommendSeriesView()
ContentMainBannerView()
.padding(.bottom, 26.7)
.padding(.horizontal, 13.3)
ContentMainRecommendSeriesView()
HStack(spacing: 8) {
ZStack {
Image("img_bg_short_play")
@ -78,9 +87,6 @@ struct ContentMainView: View {
.padding(.bottom, 40)
.padding(.horizontal, 13.3)
ContentMainMyStashView()
.padding(.horizontal, 13.3)
ContentMainNewContentView()
.padding(.horizontal, 13.3)
@ -92,6 +98,26 @@ struct ContentMainView: View {
ContentMainCurationView()
.padding(.top, 40)
.padding(.bottom, 20)
Text("""
- :
- :
- : 335 10, 5 563A호
- : 870-81-03220
- : 2024-B-1012
- : 02.2055.1477 ( 10:00~19:00)
- : sodalive.official@gmail.com
""")
.font(.custom(Font.medium.rawValue, size: 11))
.foregroundColor(Color.gray77)
.padding(.top, 13.3)
.padding(.horizontal, 13.3)
}
}
.padding(.vertical, 13.3)

View File

@ -20,6 +20,37 @@ struct SeriesContentAllView: View {
VStack(spacing: 0) {
DetailNavigationBar(title: "\(seriesTitle) - 전체회차 듣기")
HStack(spacing: 13.3) {
Spacer()
Text("최신순")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(
Color.graye2
.opacity(viewModel.sortType == .NEWEST ? 1 : 0.5)
)
.onTapGesture {
if viewModel.sortType != .NEWEST {
viewModel.sortType = .NEWEST
}
}
Text("등록순")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(
Color.graye2
.opacity(viewModel.sortType == .OLDEST ? 1 : 0.5)
)
.onTapGesture {
if viewModel.sortType != .OLDEST {
viewModel.sortType = .OLDEST
}
}
}
.padding(.vertical, 13.3)
.padding(.horizontal, 20)
.background(Color.gray16)
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 12) {
ForEach(0..<viewModel.seriesContentList.count, id: \.self) { index in

View File

@ -19,14 +19,24 @@ final class SeriesContentAllViewModel: ObservableObject {
@Published var isShowPopup = false
@Published var seriesContentList = [GetSeriesContentListItem]()
@Published var sortType: SeriesListAllViewModel.SeriesSortType = .NEWEST {
didSet {
page = 1
isLast = false
getSeriesContentList()
}
}
var page = 1
var isLast = false
private let pageSize = 10
func getSeriesContentList() {
if !isLoading && !isLast {
isLoading = true
repository
.getSeriesContentList(seriesId: seriesId, page: page, size: pageSize)
.getSeriesContentList(seriesId: seriesId, page: page, size: pageSize, sortType: sortType)
.sink { result in
switch result {
case .finished:

View File

@ -11,7 +11,7 @@ import Moya
enum SeriesApi {
case getSeriesList(creatorId: Int, sortType: SeriesListAllViewModel.SeriesSortType, page: Int, size: Int)
case getSeriesDetail(seriesId: Int)
case getSeriesContentList(seriesId: Int, page: Int, size: Int)
case getSeriesContentList(seriesId: Int, page: Int, size: Int, sortType: SeriesListAllViewModel.SeriesSortType)
case getRecommendSeriesList
}
@ -28,7 +28,7 @@ extension SeriesApi: TargetType {
case .getSeriesDetail(let seriesId):
return "/audio-content/series/\(seriesId)"
case .getSeriesContentList(let seriesId, _, _):
case .getSeriesContentList(let seriesId, _, _, _):
return "/audio-content/series/\(seriesId)/content"
case .getRecommendSeriesList:
@ -58,10 +58,11 @@ extension SeriesApi: TargetType {
case .getSeriesDetail, .getRecommendSeriesList:
return .requestPlain
case .getSeriesContentList(_, let page, let size):
case .getSeriesContentList(_, let page, let size, let sortType):
let parameters = [
"page": page - 1,
"size": size
"size": size,
"sortType": sortType
] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)

View File

@ -13,7 +13,7 @@ final class SeriesListAllViewModel: ObservableObject {
private var subscription = Set<AnyCancellable>()
enum SeriesSortType: String {
case NEWEST, POPULAR
case NEWEST, OLDEST
}
var creatorId: Int = 0

View File

@ -21,8 +21,8 @@ class SeriesRepository {
return api.requestPublisher(.getSeriesDetail(seriesId: seriesId))
}
func getSeriesContentList(seriesId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getSeriesContentList(seriesId: seriesId, page: page, size: size))
func getSeriesContentList(seriesId: Int, page: Int, size: Int, sortType: SeriesListAllViewModel.SeriesSortType) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getSeriesContentList(seriesId: seriesId, page: page, size: size, sortType: sortType))
}
func getRecommendSeriesList() -> AnyPublisher<Response, MoyaError> {

View File

@ -189,7 +189,9 @@ struct ContentView: View {
case .tempCanPayment(let orderType, let contentId, let title, let can):
CanPaymentTempView(orderType: orderType, contentId: contentId, title: title, can: can)
case .blockList:
BlockMemberListView()
default:
EmptyView()

View File

@ -14,3 +14,4 @@ let AGORA_APP_ID = "b96574e191a9430fa54c605528aa3ef7"
let AGORA_APP_CERTIFICATE = "ae18ade3afcf4086bd4397726eb0654c"
let BOOTPAY_APP_ID = "6242a7772701800023f68b2f"
let BOOTPAY_APP_HECTO_ID = "667fca5d3bab7404f831c3e5"

View File

@ -0,0 +1,82 @@
//
// CommunityPostPurchaseDialog.swift
// SodaLive
//
// Created by klaus on 5/24/24.
//
import SwiftUI
struct CommunityPostPurchaseDialog: View {
@Binding var isShowing: Bool
let can: Int
let confirmAction: () -> Void
var body: some View {
GeometryReader { geo in
ZStack {
Color.black
.opacity(0.5)
.frame(width: geo.size.width, height: geo.size.height)
VStack(spacing: 0) {
Text("게시글 보기")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.graybb)
.padding(.top, 40)
Text("게시글을\n확인하시겠습니까?")
.font(.custom(Font.medium.rawValue, size: 15))
.foregroundColor(Color.graybb)
.multilineTextAlignment(.center)
.padding(.top, 12)
.padding(.horizontal, 13.3)
HStack(spacing: 13.3) {
Text("취소")
.font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color.button)
.padding(.vertical, 16)
.frame(width: (geo.size.width - 66.7) / 3)
.background(Color.bg)
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.button, lineWidth: 1)
)
.onTapGesture {
isShowing = false
}
Text("\(can)캔으로 보기")
.font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color.white)
.padding(.vertical, 16)
.frame(width: (geo.size.width - 66.7) * 2 / 3)
.background(Color.button)
.cornerRadius(8)
.onTapGesture {
confirmAction()
isShowing = false
}
}
.padding(.top, 26.7)
.padding(.bottom, 16.7)
}
.frame(width: geo.size.width - 26.7, alignment: .center)
.background(Color.gray22)
.cornerRadius(10)
}
}
}
}
#Preview {
CommunityPostPurchaseDialog(
isShowing: .constant(true),
can: 10,
confirmAction: {}
)
}

View File

@ -0,0 +1,174 @@
//
// MemberProfileDialog.swift
// SodaLive
//
// Created by klaus on 9/7/24.
//
import SwiftUI
import Kingfisher
struct MemberProfileDialog: View {
@StateObject var viewModel = UserViewModel()
@Binding var isShowing: Bool
let memberId: Int
var body: some View {
ZStack {
Color.black.opacity(0.7).ignoresSafeArea()
.onTapGesture {
isShowing = false
}
VStack(alignment: .leading, spacing: 21) {
HStack(spacing: 0) {
Text("프로필")
.font(.custom(Font.bold.rawValue, size: 15))
.foregroundColor(Color.grayee)
Spacer()
Image("ic_close_white")
.onTapGesture {
isShowing = false
}
}
if let profile = viewModel.memberProfile {
Text(profile.nickname)
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.grayee)
KFImage(URL(string: profile.profileImageUrl))
.resizable()
.frame(maxWidth: screenSize().width - 66.7, maxHeight: screenSize().width - 66.7)
.aspectRatio(CGSize(width: 1, height: 1), contentMode: .fit)
.cornerRadius(8)
HStack(spacing: 8) {
Text(profile.isBlocked ? "차단 해제" : "차단")
.font(.custom(Font.bold.rawValue, size: 15))
.foregroundColor(Color.button)
.frame(maxWidth: .infinity)
.padding(.vertical, 13)
.cornerRadius(8)
.contentShape(Rectangle())
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color.button)
)
.onTapGesture {
if profile.isBlocked {
viewModel.memberUnBlock()
} else {
viewModel.isShowUesrBlockConfirm = true
}
}
Text("사용자 신고")
.font(.custom(Font.bold.rawValue, size: 15))
.foregroundColor(Color.button)
.frame(maxWidth: .infinity)
.padding(.vertical, 13)
.cornerRadius(8)
.contentShape(Rectangle())
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color.button)
)
.onTapGesture { viewModel.isShowUesrReportView = true }
Text("프로필 신고")
.font(.custom(Font.bold.rawValue, size: 15))
.foregroundColor(Color.button)
.frame(maxWidth: .infinity)
.padding(.vertical, 13)
.cornerRadius(8)
.contentShape(Rectangle())
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color.button)
)
.onTapGesture { viewModel.isShowProfileReportConfirm = true }
}
}
}
.padding(.horizontal, 13.3)
.padding(.top, 13.3)
.padding(.bottom, 20)
.background(Color.gray22)
.cornerRadius(8)
.padding(.horizontal, 13.3)
.frame(maxWidth: screenSize().width - 33.3)
.onAppear {
if memberId <= 1 {
viewModel.errorMessage = "잘못된 요청입니다."
viewModel.isShowPopup = true
} else {
viewModel.getMemberProfile(memberId: memberId)
}
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
.onDisappear {
if viewModel.dismissDialog {
isShowing = false
}
}
}
if viewModel.isShowUesrBlockConfirm {
UserBlockConfirmDialogView(
isShowing: $viewModel.isShowUesrBlockConfirm,
nickname: viewModel.nickname,
confirmAction: {
viewModel.memberBlock()
}
)
}
if viewModel.isShowUesrReportView {
UserReportDialogView(
isShowing: $viewModel.isShowUesrReportView,
confirmAction: { reason in
viewModel.report(type: .USER, reason: reason)
}
)
}
if viewModel.isShowProfileReportConfirm {
ProfileReportDialogView(
isShowing: $viewModel.isShowProfileReportConfirm,
confirmAction: {
viewModel.report(type: .PROFILE)
}
)
}
if viewModel.isLoading {
LoadingView()
}
}
}
}
#Preview {
MemberProfileDialog(isShowing: .constant(true), memberId: 1)
}

View File

@ -42,13 +42,13 @@ struct SodaDialog: View {
VStack(spacing: 0) {
Text(title)
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "bbbbbb"))
.foregroundColor(Color.graybb)
.padding(.top, 40)
Text(desc)
.font(.custom(Font.medium.rawValue, size: 15))
.foregroundColor(Color(hex: "bbbbbb"))
.multilineTextAlignment(.center)
.foregroundColor(Color.graybb)
.multilineTextAlignment(.leading)
.padding(.top, 12)
.padding(.horizontal, 13.3)
.fixedSize(horizontal: false, vertical: true)
@ -57,14 +57,14 @@ struct SodaDialog: View {
if cancelButtonTitle.count > 0 {
Text(cancelButtonTitle)
.font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color(hex: "3bb9f1"))
.foregroundColor(Color.button)
.padding(.vertical, 16)
.frame(width: (geo.size.width - 66.7) / 3)
.background(Color(hex: "13181b"))
.background(Color.bg)
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color(hex: "3bb9f1"), lineWidth: 1)
.stroke(Color.button, lineWidth: 1)
)
.onTapGesture {
cancelButtonAction()
@ -73,10 +73,10 @@ struct SodaDialog: View {
Text(confirmButtonTitle)
.font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color(hex: "ffffff"))
.foregroundColor(Color.white)
.padding(.vertical, 16)
.frame(width: (geo.size.width - 66.7) * 2 / 3)
.background(Color(hex: "3bb9f1"))
.background(Color.button)
.cornerRadius(8)
.onTapGesture {
confirmButtonAction()
@ -86,7 +86,7 @@ struct SodaDialog: View {
.padding(.bottom, 16.7)
}
.frame(width: geo.size.width - 26.7, alignment: .center)
.background(Color(hex: "222222"))
.background(Color.gray22)
.cornerRadius(10)
}
}

View File

@ -17,6 +17,7 @@ struct CreatorCommunityCommentItemView: View {
let modifyComment: (Int, String) -> Void
let onClickDelete: (Int) -> Void
let onClickProfile: (Int) -> Void
@State var isShowPopupMenu: Bool = false
@State var isModeModify: Bool = false
@ -30,6 +31,11 @@ struct CreatorCommunityCommentItemView: View {
.resizable()
.frame(width: 40, height: 40)
.clipShape(Circle())
.onTapGesture {
if UserDefaults.int(forKey: .userId) != commentItem.writerId {
onClickProfile(commentItem.writerId)
}
}
VStack(alignment: .leading, spacing: 0) {
Text(commentItem.nickname)
@ -38,7 +44,7 @@ struct CreatorCommunityCommentItemView: View {
Text(commentItem.date)
.font(.custom(Font.medium.rawValue, size: 10.3))
.foregroundColor(Color(hex: "525252"))
.foregroundColor(Color.gray52)
.padding(.top, 4)
}
@ -57,8 +63,8 @@ struct CreatorCommunityCommentItemView: View {
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.accentColor(Color(hex: "3bb9f1"))
.foregroundColor(Color.grayee)
.accentColor(Color.button)
.keyboardType(.default)
.padding(.horizontal, 13.3)

View File

@ -17,6 +17,10 @@ struct CreatorCommunityCommentListView: View {
@State private var commentId: Int = 0
@State private var isShowDeletePopup: Bool = false
@State private var memberId: Int = 0
@State private var isShowMemberProfilePopup: Bool = false
@StateObject var viewModel = CreatorCommunityCommentListViewModel()
var body: some View {
@ -115,6 +119,10 @@ struct CreatorCommunityCommentListView: View {
onClickDelete: {
commentId = $0
isShowDeletePopup = true
},
onClickProfile: {
memberId = $0
isShowMemberProfilePopup = true
}
)
.padding(.horizontal, 26.7)
@ -147,6 +155,10 @@ struct CreatorCommunityCommentListView: View {
)
}
if isShowMemberProfilePopup {
MemberProfileDialog(isShowing: $isShowMemberProfilePopup, memberId: memberId)
}
if viewModel.isLoading {
LoadingView()
}

View File

@ -20,6 +20,9 @@ struct CreatorCommunityCommentReplyView: View {
@State private var commentId: Int = 0
@State private var isShowDeletePopup: Bool = false
@State private var memberId: Int = 0
@State private var isShowMemberProfilePopup: Bool = false
var body: some View {
ZStack {
VStack(spacing: 0) {
@ -98,7 +101,11 @@ struct CreatorCommunityCommentReplyView: View {
isReplyComment: true,
isShowPopupMenuButton: false,
modifyComment: { _, _ in },
onClickDelete: { _ in }
onClickDelete: { _ in },
onClickProfile: {
memberId = $0
isShowMemberProfilePopup = true
}
)
.padding(.horizontal, 26.7)
.padding(.bottom, 13.3)
@ -120,6 +127,10 @@ struct CreatorCommunityCommentReplyView: View {
onClickDelete: {
commentId = $0
isShowDeletePopup = true
},
onClickProfile: {
memberId = $0
isShowMemberProfilePopup = true
}
)
.padding(.horizontal, 26.7)
@ -154,6 +165,10 @@ struct CreatorCommunityCommentReplyView: View {
)
}
if isShowMemberProfilePopup {
MemberProfileDialog(isShowing: $isShowMemberProfilePopup, memberId: memberId)
}
if viewModel.isLoading {
LoadingView()
}

View File

@ -0,0 +1,39 @@
//
// CreatorCommunityAllItemLockView.swift
// SodaLive
//
// Created by klaus on 5/24/24.
//
import SwiftUI
struct CreatorCommunityAllItemLockView: View {
let price: Int
let onClickPurchaseContent: () -> Void
var body: some View {
VStack(spacing: 26.7) {
Image("ic_lock_bb")
Text("\(price)캔으로 게시글 보기")
.font(.custom(Font.bold.rawValue, size: 12))
.foregroundColor(Color.button)
.padding(.horizontal, 21)
.padding(.vertical, 11)
.overlay(
RoundedRectangle(cornerRadius: 26.7)
.stroke(Color.button, lineWidth: 1)
)
.onTapGesture { onClickPurchaseContent() }
}
.frame(width: screenSize().width - 42, height: screenSize().width - 42)
.background(Color.gray33)
.cornerRadius(5.3)
}
}
#Preview {
CreatorCommunityAllItemLockView(price: 100) {
}
}

View File

@ -15,23 +15,29 @@ struct CreatorCommunityAllItemView: View {
let onClickComment: () -> Void
let onClickWriteComment: (String) -> Void
let onClickShowReportMenu: () -> Void
let onClickPurchaseContent: () -> Void
@State var isLike = false
@State var likeCount = 0
@State private var textHeight: CGFloat = .zero
@StateObject var playManager = CreatorCommunityMediaPlayerManager.shared
@StateObject var contentPlayManager = ContentPlayManager.shared
init(
item: GetCommunityPostListResponse,
onClickLike: @escaping () -> Void,
onClickComment: @escaping () -> Void,
onClickWriteComment: @escaping (String) -> Void,
onClickShowReportMenu: @escaping () -> Void
onClickShowReportMenu: @escaping () -> Void,
onClickPurchaseContent: @escaping () -> Void
) {
self.item = item
self.onClickLike = onClickLike
self.onClickComment = onClickComment
self.onClickWriteComment = onClickWriteComment
self.onClickShowReportMenu = onClickShowReportMenu
self.onClickPurchaseContent = onClickPurchaseContent
self._isLike = State(initialValue: item.isLike)
self._likeCount = State(initialValue: item.likeCount)
@ -58,72 +64,91 @@ struct CreatorCommunityAllItemView: View {
Spacer()
Image("ic_seemore_vertical")
.padding(.trailing, 8.3)
.onTapGesture { onClickShowReportMenu() }
if item.price <= 0 || item.existOrdered {
Image("ic_seemore_vertical")
.padding(.trailing, 8.3)
.onTapGesture { onClickShowReportMenu() }
}
}
DetectableTextView(text: item.content, textSize: 13.3, font: Font.medium.rawValue)
.frame(
width: screenSize().width - 16,
width: screenSize().width - 42,
height: textHeight
)
.onAppear {
self.textHeight = self.estimatedHeight(
for: item.content,
width: screenSize().width - 16
width: screenSize().width - 42
)
}
.onChange(of: item.content) { newText in
self.textHeight = self.estimatedHeight(
for: newText,
width: screenSize().width - 16
width: screenSize().width - 42
)
}
if let imageUrl = item.imageUrl {
KFImage(URL(string: imageUrl))
.resizable()
.frame(maxWidth: .infinity)
.scaledToFit()
}
HStack(spacing: 8) {
IconAndTitleToggleButton(
isChecked: isLike,
title: "\(likeCount)",
normalIconName: "ic_audio_content_heart_normal",
checkedIconName: "ic_audio_content_heart_pressed"
) {
if isLike {
isLike = false
likeCount -= 1
} else {
isLike = true
likeCount += 1
}
onClickLike()
}
}
.frame(maxWidth: .infinity, alignment: .leading)
if item.isCommentAvailable {
CreatorCommunityCommentView(
commentCount: item.commentCount,
commentItem: item.firstComment,
onClickWriteComment: onClickWriteComment
)
.onTapGesture {
if item.commentCount > 0 {
onClickComment()
if item.price <= 0 || item.existOrdered {
if let imageUrl = item.imageUrl {
ZStack {
KFImage(URL(string: imageUrl))
.resizable()
.frame(maxWidth: .infinity)
.scaledToFit()
if let audioUrl = item.audioUrl {
Image(playManager.isPlaying && playManager.currentPlayingContentId == item.postId ? "btn_audio_content_pause" : "btn_audio_content_play")
.onTapGesture {
contentPlayManager.pauseAudio()
playManager.toggleContent(item: CreatorCommunityContentItem(contentId: item.postId, url: audioUrl))
}
}
}
}
HStack(spacing: 8) {
IconAndTitleToggleButton(
isChecked: isLike,
title: "\(likeCount)",
normalIconName: "ic_audio_content_heart_normal",
checkedIconName: "ic_audio_content_heart_pressed"
) {
if isLike {
isLike = false
likeCount -= 1
} else {
isLike = true
likeCount += 1
}
onClickLike()
}
}
.frame(maxWidth: .infinity, alignment: .leading)
if item.isCommentAvailable {
CreatorCommunityCommentView(
commentCount: item.commentCount,
commentItem: item.firstComment,
onClickWriteComment: onClickWriteComment
)
.onTapGesture {
if item.commentCount > 0 {
onClickComment()
}
}
}
} else {
CreatorCommunityAllItemLockView(
price: item.price,
onClickPurchaseContent: onClickPurchaseContent)
}
}
.padding(.horizontal, 8)
.padding(.vertical, 11)
.background(Color.gray22)
.cornerRadius(5.3)
.padding(.horizontal, 13.3)
}
private func estimatedHeight(for text: String, width: CGFloat) -> CGFloat {
@ -143,11 +168,14 @@ struct CreatorCommunityAllItemView_Previews: PreviewProvider {
creatorNickname: "민하나",
creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
audioUrl: nil,
content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!",
price: 10,
date: "3일전",
isCommentAvailable: false,
isAdult: false,
isLike: true,
existOrdered: false,
likeCount: 10,
commentCount: 0,
firstComment: nil
@ -155,7 +183,8 @@ struct CreatorCommunityAllItemView_Previews: PreviewProvider {
onClickLike: {},
onClickComment: {},
onClickWriteComment: { _ in },
onClickShowReportMenu: {}
onClickShowReportMenu: {},
onClickPurchaseContent: {}
)
}
}

View File

@ -12,6 +12,7 @@ struct CreatorCommunityAllView: View {
let creatorId: Int
@StateObject var viewModel = CreatorCommunityAllViewModel()
@StateObject var playerManager = CreatorCommunityMediaPlayerManager.shared
var body: some View {
GeometryReader { proxy in
@ -41,6 +42,12 @@ struct CreatorCommunityAllView: View {
onClickShowReportMenu: {
viewModel.postId = item.postId
viewModel.isShowReportMenu = true
},
onClickPurchaseContent: {
viewModel.postId = item.postId
viewModel.postPrice = item.price
viewModel.postIndex = index
viewModel.isShowPostPurchaseView = true
}
)
.onAppear {
@ -50,7 +57,6 @@ struct CreatorCommunityAllView: View {
}
}
}
.padding(5.3)
}
}
.sheet(
@ -121,6 +127,19 @@ struct CreatorCommunityAllView: View {
}
)
}
if viewModel.isShowPostPurchaseView {
CommunityPostPurchaseDialog(
isShowing: $viewModel.isShowPostPurchaseView,
can: viewModel.postPrice
) {
viewModel.purchaseCommunityPost()
}
}
if playerManager.isLoading {
LoadingView()
}
}
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
@ -131,7 +150,24 @@ struct CreatorCommunityAllView: View {
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff"))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
.popup(isPresented: $playerManager.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
GeometryReader { geo in
HStack {
Spacer()
Text(playerManager.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
@ -144,6 +180,9 @@ struct CreatorCommunityAllView: View {
viewModel.creatorId = creatorId
viewModel.getCommunityPostList()
}
.onDisappear {
CreatorCommunityMediaPlayerManager.shared.stopContent()
}
}
}

View File

@ -21,6 +21,8 @@ class CreatorCommunityAllViewModel: ObservableObject {
@Published private(set) var communityPostList = [GetCommunityPostListResponse]()
@Published var postId = 0
@Published var postPrice = 0
@Published var postIndex = -1
@Published var isShowCommentListView = false {
didSet {
@ -46,6 +48,16 @@ class CreatorCommunityAllViewModel: ObservableObject {
}
}
@Published var isShowPostPurchaseView = false {
didSet {
if !isShowPostPurchaseView {
postId = 0
postPrice = 0
postIndex = -1
}
}
}
var creatorId = 0
var page = 1
@ -254,4 +266,51 @@ class CreatorCommunityAllViewModel: ObservableObject {
self.isLoading = false
}
}
func purchaseCommunityPost() {
let postId = postId
let postIndex = postIndex
if !isLoading {
isLoading = true
repository
.purchaseCommunityPost(postId: postId)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { [unowned self] response in
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<GetCommunityPostListResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
if postIndex >= 0 {
communityPostList[postIndex] = data
}
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
self.isLoading = false
}
.store(in: &subscription)
}
}
}

View File

@ -0,0 +1,130 @@
//
// CreatorCommunityMediaPlayerManager.swift
// SodaLive
//
// Created by klaus on 8/7/24.
//
import Foundation
import AVKit
import MediaPlayer
final class CreatorCommunityMediaPlayerManager: NSObject, ObservableObject {
static let shared = CreatorCommunityMediaPlayerManager()
@Published private (set) var currentPlayingContentId: Int = 0
@Published private (set) var isPlaying = false
@Published var isLoading = false
@Published var errorMessage = ""
@Published var isShowPopup = false
private var player: AVAudioPlayer!
private func playContent(item: CreatorCommunityContentItem) {
if item.contentId <= 0 {
return
}
if (currentPlayingContentId == item.contentId && !isPlaying) {
resumeContent()
return
}
stopContent()
currentPlayingContentId = item.contentId
guard let url = URL(string: item.url) else {
showError()
return
}
isLoading = true
URLSession.shared.dataTask(with: url) { [unowned self] data, response, error in
guard let audioData = data else {
self.isLoading = false
return
}
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .moviePlayback)
try audioSession.setActive(true)
self.player = try AVAudioPlayer(data: audioData)
DispatchQueue.main.async {
self.player?.volume = 1
self.player?.delegate = self
self.player?.prepareToPlay()
self.player?.play()
self.isPlaying = self.player.isPlaying
}
} catch {
DispatchQueue.main.async {
self.showError()
}
}
DispatchQueue.main.async {
self.isLoading = false
}
}.resume()
}
private func resumeContent() {
if let player = player {
player.play()
isPlaying = player.isPlaying
}
}
private func showError() {
self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
self.isShowPopup = true
}
}
extension CreatorCommunityMediaPlayerManager {
func toggleContent(item: CreatorCommunityContentItem) {
if currentPlayingContentId == item.contentId {
if let player = player, player.isPlaying {
pauseContent()
} else {
resumeContent()
}
} else {
playContent(item: item)
}
}
func pauseContent() {
if let player = player {
player.pause()
isPlaying = player.isPlaying
}
}
func stopContent() {
if let player = player {
player.stop()
player.currentTime = 0
}
isPlaying = false
currentPlayingContentId = 0
}
}
extension CreatorCommunityMediaPlayerManager: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
stopContent()
}
}
struct CreatorCommunityContentItem {
let contentId: Int
let url: String
}

View File

@ -0,0 +1,14 @@
//
// PurchasePostRequest.swift
// SodaLive
//
// Created by klaus on 5/24/24.
//
import Foundation
struct PurchasePostRequest: Encodable {
let postId: Int
let container: String = "ios"
let timezone: String = TimeZone.current.identifier
}

View File

@ -19,6 +19,7 @@ enum CreatorCommunityApi {
case getCommentReplyList(commentId: Int, page: Int, size: Int)
case modifyComment(request: ModifyCommunityPostCommentRequest)
case getLatestPostListFromCreatorsYouFollow
case purchaseCommunityPost(postId: Int)
}
extension CreatorCommunityApi: TargetType {
@ -48,12 +49,15 @@ extension CreatorCommunityApi: TargetType {
case .getLatestPostListFromCreatorsYouFollow:
return "/creator-community/latest"
case .purchaseCommunityPost:
return "/creator-community/purchase"
}
}
var method: Moya.Method {
switch self {
case .createCommunityPost, .communityPostLike, .createCommunityPostComment:
case .createCommunityPost, .communityPostLike, .createCommunityPostComment, .purchaseCommunityPost:
return .post
case .getCommunityPostList, .getCommunityPostCommentList, .getCommentReplyList, .getCommunityPostDetail, .getLatestPostListFromCreatorsYouFollow:
@ -115,6 +119,9 @@ extension CreatorCommunityApi: TargetType {
case .getLatestPostListFromCreatorsYouFollow:
let parameters = ["timezone": TimeZone.current.identifier] as [String: Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .purchaseCommunityPost(let postId):
return .requestJSONEncodable(PurchasePostRequest(postId: postId))
}
}

View File

@ -22,19 +22,19 @@ struct CreatorCommunityItemView: View {
Text(item.creatorNickname)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
Spacer()
Text(item.date)
.font(.custom(Font.light.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
}
HStack(spacing: 0) {
Text(item.content)
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "bbbbbb"))
.foregroundColor(Color.graybb)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(3)
@ -45,9 +45,10 @@ struct CreatorCommunityItemView: View {
.resizable()
.frame(width: 53.3, height: 53.3)
.cornerRadius(4.7)
.blur(radius: item.existOrdered || item.price <= 0 ? 0 : 15)
} else {
Rectangle()
.foregroundColor(Color(hex: "222222").opacity(0))
.foregroundColor(Color.gray22.opacity(0))
.frame(width: 53.3, height: 53.3)
}
}
@ -60,7 +61,7 @@ struct CreatorCommunityItemView: View {
Text("\(item.likeCount)")
.font(.custom(Font.medium.rawValue, size: 11))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
}
HStack(spacing: 6) {
@ -70,13 +71,13 @@ struct CreatorCommunityItemView: View {
Text("\(item.commentCount)")
.font(.custom(Font.medium.rawValue, size: 11))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(13.3)
.background(Color(hex: "222222"))
.background(Color.gray22)
.cornerRadius(11)
}
}
@ -90,11 +91,14 @@ struct CreatorCommunityItemView_Previews: PreviewProvider {
creatorNickname: "민하나",
creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
audioUrl: nil,
content: "안녕하세요",
price: 10,
date: "3일전",
isCommentAvailable: false,
isAdult: false,
isLike: false,
existOrdered: false,
likeCount: 10,
commentCount: 0,
firstComment: nil

View File

@ -52,4 +52,8 @@ class CreatorCommunityRepository {
func getLatestPostListFromCreatorsYouFollow() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getLatestPostListFromCreatorsYouFollow)
}
func purchaseCommunityPost(postId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.purchaseCommunityPost(postId: postId))
}
}

View File

@ -11,11 +11,14 @@ struct GetCommunityPostListResponse: Decodable {
let creatorNickname: String
let creatorProfileUrl: String
let imageUrl: String?
let audioUrl: String?
let content: String
let price: Int
let date: String
let isCommentAvailable: Bool
let isAdult: Bool
let isLike: Bool
let existOrdered: Bool
let likeCount: Int
let commentCount: Int
let firstComment: GetCommunityPostCommentListItem?

View File

@ -9,6 +9,7 @@ import Foundation
struct CreateCommunityPostRequest: Encodable {
let content: String
let price: Int
let isAdult: Bool
let isCommentAvailable: Bool
}

View File

@ -0,0 +1,210 @@
//
// CreatorCommunityRecordingVoiceView.swift
// SodaLive
//
// Created by klaus on 8/7/24.
//
import SwiftUI
struct CreatorCommunityRecordingVoiceView: View {
@StateObject var soundManager = CreatorCommunitySoundManager()
@Binding var isShowing: Bool
@Binding var isShowPopup: Bool
@Binding var errorMessage: String
@Binding var fileName: String
@Binding var soundData: Data?
@State private var tempFileName = ""
var body: some View {
ZStack {
Color.black.opacity(0.7)
.ignoresSafeArea()
GeometryReader { proxy in
VStack {
Spacer()
VStack {
VStack(spacing: 0) {
HStack(spacing: 0) {
Text("음성녹음")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(.white)
Spacer()
Image("ic_close_white")
.resizable()
.frame(width: 20, height: 20)
.onTapGesture { isShowing = false }
}
.padding(.horizontal, 26.7)
.padding(.top, 26.7)
}
Text(soundManager.timeString)
.font(.custom(Font.bold.rawValue, size: 33.3))
.foregroundColor(.white)
.padding(.top, 80)
switch soundManager.recordMode {
case .RECORD:
if !soundManager.isLoading {
Image(soundManager.isRecording ? "ic_record_stop" : "ic_record")
.resizable()
.frame(width: 70, height: 70)
.padding(.vertical, 52.3)
.onTapGesture {
if !soundManager.isLoading {
if !soundManager.isRecording {
tempFileName = "now_voice_\(Int(Date().timeIntervalSince1970 * 1000)).m4a"
soundManager.startRecording(tempFileName)
} else {
soundManager.stopRecording()
soundManager.recordMode = .PLAY
}
}
}
}
case .PLAY:
if !soundManager.isLoading {
VStack(spacing: 0) {
HStack(spacing: 0) {
Spacer()
Text("삭제")
.font(.custom(Font.medium.rawValue, size: 15.3))
.foregroundColor(Color.graybb.opacity(0))
Spacer()
Image(
!soundManager.isPlaying ?
"ic_record_play" :
"ic_record_pause"
)
.onTapGesture {
if !soundManager.isLoading {
if !soundManager.isPlaying {
soundManager.playAudio()
} else {
soundManager.stopAudio()
}
}
}
Spacer()
Text("삭제")
.font(.custom(Font.medium.rawValue, size: 15.3))
.foregroundColor(Color.graybb)
.onTapGesture {
soundManager.stopAudio()
soundManager.deleteAudioFile()
soundManager.recordMode = .RECORD
}
Spacer()
}
.padding(.vertical, 52.3)
HStack(spacing: 13.3) {
Text("다시 녹음")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.button)
.frame(width: (proxy.size.width - 40) / 3, height: 50)
.background(Color.button.opacity(0.2))
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.button, lineWidth: 1.3)
)
.onTapGesture {
soundManager.stopAudio()
soundManager.deleteAudioFile()
soundManager.recordMode = .RECORD
}
Text("녹음완료")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(.white)
.frame(width: (proxy.size.width - 40) * 2 / 3, height: 50)
.background(Color.button)
.cornerRadius(10)
.onTapGesture {
do {
let soundData = try Data(contentsOf: soundManager.getAudioFileURL())
self.soundData = soundData
self.fileName = tempFileName
self.isShowing = false
} catch {
errorMessage = "녹음파일을 생성하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
isShowPopup = true
}
}
}
.padding(.bottom, 40)
.padding(.horizontal, 13.3)
}
}
}
if proxy.safeAreaInsets.bottom > 0 {
Rectangle()
.foregroundColor(Color.gray22)
.frame(width: proxy.size.width, height: 15.3)
}
if soundManager.isLoading {
LoadingView()
}
}
.background(Color(hex: "222222"))
.cornerRadius(16.7, corners: [.topLeft, .topRight])
}
.edgesIgnoringSafeArea(.bottom)
.onAppear {
soundManager.prepareRecording()
}
}
}
.popup(isPresented: $soundManager.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
GeometryReader { geo in
HStack {
Spacer()
Text(soundManager.errorMessage)
.padding(.vertical, 13.3)
.padding(.horizontal, 6.7)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
.onDisappear {
if soundManager.onClose {
isShowing = false
}
}
}
}
}
}
#Preview {
CreatorCommunityRecordingVoiceView(
isShowing: .constant(false),
isShowPopup: .constant(false),
errorMessage: .constant(""),
fileName: .constant(""),
soundData: .constant(nil)
)
}

View File

@ -0,0 +1,205 @@
//
// CreatorCommunitySoundManager.swift
// SodaLive
//
// Created by klaus on 8/7/24.
//
import Foundation
import AVKit
import Combine
class CreatorCommunitySoundManager: NSObject, ObservableObject {
enum RecordMode {
case RECORD, PLAY
}
@Published var recordMode = RecordMode.RECORD
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var isLoading = false
@Published var onClose = false
@Published var isPlaying = false
@Published var isRecording = false
@Published var timeString = "00:00.00"
private var timerSubscription: Cancellable?
private var startTime: Date?
var player: AVAudioPlayer!
var audioRecorder: AVAudioRecorder!
var fileName = "now_voice.m4a"
let audioSession = AVAudioSession.sharedInstance()
func prepareRecording() {
isLoading = true
do {
try audioSession.setCategory(.playAndRecord, mode: .videoRecording)
try audioSession.setActive(true)
audioSession.requestRecordPermission() { [weak self] allowed in
DispatchQueue.main.async {
if !allowed {
self?.errorMessage = "권한을 허용하지 않으시면 음성녹음을 하실 수 없습니다."
self?.isShowPopup = true
self?.onClose = true
}
}
}
} catch {
errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
isShowPopup = true
onClose = true
}
isLoading = false
}
func startRecording(_ fileName: String) {
self.fileName = fileName
player?.stop()
player = nil
isLoading = true
let settings = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 48000,
AVEncoderBitRateKey: 256000,
AVNumberOfChannelsKey: 2,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
do {
try audioSession.setCategory(.playAndRecord, mode: .videoRecording)
try audioSession.setActive(true)
audioRecorder = try AVAudioRecorder(url: getAudioFileURL(), settings: settings)
audioRecorder.record()
isRecording = true
startTimer()
} catch {
errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
isShowPopup = true
}
isLoading = false
}
func stopRecording() {
stopTimer()
audioRecorder?.stop()
audioRecorder = nil
isRecording = false
prepareForPlay()
}
func prepareForPlay(_ url: URL? = nil) {
isLoading = true
DispatchQueue.main.async {
do {
try self.audioSession.setCategory(.playback, mode: .moviePlayback)
try self.audioSession.setActive(true)
if let url = url {
self.player = try AVAudioPlayer(data: Data(contentsOf: url))
} else {
self.player = try AVAudioPlayer(contentsOf: self.getAudioFileURL())
}
self.player?.volume = 1
self.player?.delegate = self
self.player?.prepareToPlay()
} catch {
self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
self.isShowPopup = true
}
self.isLoading = false
}
}
func playAudio() {
player?.play()
isPlaying = player.isPlaying
startTimer()
}
func stopAudio() {
stopTimer()
player?.stop()
player.currentTime = 0
isPlaying = player.isPlaying
}
func deleteAudioFile() {
do {
try FileManager.default.removeItem(at: getAudioFileURL())
} catch {}
}
func getAudioFileURL() -> URL {
return getDocumentsDirectory().appendingPathComponent(fileName)
}
private func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
func startTimer() {
timeString = "00:00.00"
startTime = Date()
timerSubscription = Timer.publish(every: 0.01, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.updateTime()
}
}
func stopTimer() {
timeString = "00:00.00"
timerSubscription?.cancel()
startTime = nil
timerSubscription = nil
}
private func updateTime() {
guard let startTime = startTime else { return }
let elapsedTime = Date().timeIntervalSince(startTime)
let minutes = Int(elapsedTime) / 60
let seconds = Int(elapsedTime) % 60
let centiseconds = Int((elapsedTime - Double(minutes * 60) - Double(seconds)) * 100)
timeString = String(format: "%02d:%02d.%02d", minutes, seconds, centiseconds)
if minutes >= 3 {
stopRecording()
recordMode = .PLAY
}
}
deinit {
player?.stop()
audioRecorder?.stop()
startTime = nil
timerSubscription?.cancel()
}
}
extension CreatorCommunitySoundManager: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
stopAudio()
}
}

View File

@ -12,7 +12,9 @@ struct CreatorCommunityWriteView: View {
@StateObject var keyboardHandler = KeyboardHandler()
@StateObject private var viewModel = CreatorCommunityWriteViewModel()
@State private var isShowRecordingVoiceView = false
@State private var isShowPhotoPicker = false
@State private var fileName: String = "녹음"
let onSuccess: () -> Void
var body: some View {
@ -27,7 +29,7 @@ struct CreatorCommunityWriteView: View {
VStack(spacing: 0) {
Text("이미지")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
.frame(maxWidth: .infinity, alignment: .leading)
ZStack {
@ -45,14 +47,14 @@ struct CreatorCommunityWriteView: View {
.scaledToFit()
.padding(13.3)
.frame(width: 107, height: 107)
.background(Color(hex: "13181B"))
.background(Color.bg)
.cornerRadius(8)
.clipped()
}
Image("ic_camera")
.padding(10)
.background(Color(hex: "3BB9F1"))
.background(Color.button)
.cornerRadius(30)
.offset(x: 50, y: 36)
}
@ -63,28 +65,60 @@ struct CreatorCommunityWriteView: View {
HStack(alignment: .top, spacing: 0) {
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
Text("등록할 이미지가 없으면 이미지 없이 게시글만 등록 하셔도 됩니다.")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
}
.frame(maxWidth: .infinity, alignment: .leading)
.frame(maxWidth: .infinity)
.padding(.top, 24)
if let _ = viewModel.postImage {
VStack(spacing: 13.3) {
HStack(spacing: 0) {
Text("오디오 녹음")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color.grayee)
Spacer()
}
Text(fileName)
.font(.custom(Font.medium.rawValue, size: 16.7))
.foregroundColor(Color.main)
.padding(.vertical, 8)
.background(Color.bg)
.cornerRadius(5.3)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.overlay(
RoundedRectangle(cornerRadius: 5.3)
.stroke(Color.button, lineWidth: 1)
)
.onTapGesture { isShowRecordingVoiceView = true }
Text("※ 오디오 녹음은 최대 3분입니다")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color.gray77)
}
.padding(.top, 24)
}
HStack(spacing: 0) {
Text("내용")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
Spacer()
Text("\(viewModel.content.count)")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "ff5c49")) +
.foregroundColor(Color.mainRed) +
Text(" / 최대 500자")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
}
.padding(.top, 26.7)
@ -101,7 +135,7 @@ struct CreatorCommunityWriteView: View {
VStack(spacing: 13.3) {
Text("댓글 가능 여부")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) {
@ -126,33 +160,92 @@ struct CreatorCommunityWriteView: View {
}
.padding(.top, 26.7)
VStack(spacing: 13.3) {
Text("연령 제한")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) {
SelectButtonView(
title: "전체 연령",
isChecked: !viewModel.isAdult
) {
if viewModel.isAdult {
viewModel.isAdult = false
}
}
if UserDefaults.bool(forKey: .auth) {
VStack(spacing: 13.3) {
Text("연령 제한")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color.grayee)
.frame(maxWidth: .infinity, alignment: .leading)
SelectButtonView(
title: "19세 이상",
isChecked: viewModel.isAdult
) {
if !viewModel.isAdult {
viewModel.isAdult = true
HStack(spacing: 13.3) {
SelectButtonView(
title: "전체 연령",
isChecked: !viewModel.isAdult
) {
if viewModel.isAdult {
viewModel.isAdult = false
}
}
SelectButtonView(
title: "19세 이상",
isChecked: viewModel.isAdult
) {
if !viewModel.isAdult {
viewModel.isAdult = true
}
}
}
}
.padding(.top, 26.7)
}
if let _ = viewModel.postImage {
VStack(spacing: 13.3) {
Text("가격 설정")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color.grayee)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) {
SelectButtonView(
title: "무료",
isChecked: viewModel.isPriceFree
) {
if !viewModel.isPriceFree {
viewModel.isPriceFree = true
}
}
SelectButtonView(
title: "유료",
isChecked: !viewModel.isPriceFree
) {
if viewModel.isPriceFree {
viewModel.isPriceFree = false
}
}
}
if !viewModel.isPriceFree {
HStack(spacing: 0) {
TextField("", text: $viewModel.priceString)
.autocapitalization(.none)
.disableAutocorrection(true)
.multilineTextAlignment(.center)
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.button)
.accentColor(Color.button)
.keyboardType(.numberPad)
Spacer()
Text("")
.font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color.button)
}
.padding(.horizontal, 13.3)
.padding(.vertical, 16.7)
.frame(maxWidth: .infinity)
.overlay(
RoundedRectangle(cornerRadius: 6.7)
.stroke(Color.gray77, lineWidth: 1)
)
.background(Color.gray23)
}
}
.padding(.top, 26.7)
}
.padding(.top, 26.7)
}
.padding(13.3)
@ -160,14 +253,14 @@ struct CreatorCommunityWriteView: View {
HStack(spacing: 13.3) {
Text("닫기")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "3BB9F1"))
.foregroundColor(Color.button)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color(hex: "13181B"))
.background(Color.bg)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color(hex: "3BB9F1"), lineWidth: 1)
.stroke(Color.button, lineWidth: 1)
)
.onTapGesture {
hideKeyboard()
@ -179,11 +272,12 @@ struct CreatorCommunityWriteView: View {
.foregroundColor(Color.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color(hex: "3BB9F1"))
.background(Color.button)
.cornerRadius(10)
.onTapGesture {
hideKeyboard()
viewModel.createCommunityPost {
deleteAudioFile()
AppState.shared.back()
DispatchQueue.main.async {
@ -194,17 +288,17 @@ struct CreatorCommunityWriteView: View {
}
.padding(13.3)
.frame(maxWidth: .infinity)
.background(Color(hex: "222222"))
.background(Color.gray22)
.cornerRadius(16.7, corners: [.topLeft, .topRight])
Rectangle()
.foregroundColor(Color(hex: "222222"))
.foregroundColor(Color.gray22)
.frame(height: keyboardHandler.keyboardHeight)
.frame(maxWidth: .infinity)
if proxy.safeAreaInsets.bottom > 0 {
Rectangle()
.foregroundColor(Color(hex: "222222"))
.foregroundColor(Color.gray22)
.frame(height: 15.3)
.frame(maxWidth: .infinity)
}
@ -221,6 +315,16 @@ struct CreatorCommunityWriteView: View {
sourceType: .photoLibrary
)
}
if isShowRecordingVoiceView {
CreatorCommunityRecordingVoiceView(
isShowing: $isShowRecordingVoiceView,
isShowPopup: $viewModel.isShowPopup,
errorMessage: $viewModel.errorMessage,
fileName: $fileName,
soundData: $viewModel.soundData
)
}
}
.onTapGesture { hideKeyboard() }
.edgesIgnoringSafeArea(.bottom)
@ -232,7 +336,7 @@ struct CreatorCommunityWriteView: View {
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff"))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
@ -244,6 +348,21 @@ struct CreatorCommunityWriteView: View {
}
}
}
private func deleteAudioFile() {
do {
try FileManager.default.removeItem(at: getAudioFileURL())
} catch {}
}
private func getAudioFileURL() -> URL {
return getDocumentsDirectory().appendingPathComponent(fileName)
}
private func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
}
struct CreatorCommunityWriteView_Previews: PreviewProvider {

View File

@ -20,16 +20,39 @@ final class CreatorCommunityWriteViewModel: ObservableObject {
@Published var content = ""
@Published var isAdult = false
@Published var isPriceFree = true {
didSet {
if isPriceFree {
priceString = "0"
}
}
}
@Published var isAvailableComment = true
@Published var postImage: UIImage? = nil
@Published var priceString = "0" {
didSet {
if priceString.count > 5 {
priceString = String(priceString.prefix(5))
} else {
if let price = Int(priceString) {
self.price = price
} else {
self.price = 0
}
}
}
}
@Published var price = 0
@Published var soundData: Data? = nil
var placeholder = "내용을 입력하세요"
func createCommunityPost(onSuccess: @escaping () -> Void) {
if !isLoading && validateData() {
isLoading = true
let request = CreateCommunityPostRequest(content: content, isAdult: isAdult, isCommentAvailable: isAvailableComment)
let request = CreateCommunityPostRequest(content: content, price: price, isAdult: isAdult, isCommentAvailable: isAvailableComment)
var multipartData = [MultipartFormData]()
let encoder = JSONEncoder()
@ -43,7 +66,19 @@ final class CreatorCommunityWriteViewModel: ObservableObject {
provider: .data(imageData),
name: "postImage",
fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).jpg",
mimeType: "image/*")
mimeType: "image/*"
)
)
}
if let soundData = soundData {
multipartData.append(
MultipartFormData(
provider: .data(soundData),
name: "audioFile",
fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).m4a",
mimeType: "audio/m4a"
)
)
}
@ -103,6 +138,12 @@ final class CreatorCommunityWriteViewModel: ObservableObject {
return false
}
if !isPriceFree && price < 5 {
errorMessage = "최소금액은 5캔 입니다."
isShowPopup = true
return false
}
return true
}
}

View File

@ -16,6 +16,9 @@ struct UserProfileFanTalkAllView: View {
@State private var cheersContent: String = ""
@State private var cheersId: Int = 0
@State private var memberId: Int = 0
@State private var isShowMemberProfilePopup: Bool = false
var body: some View {
GeometryReader { proxy in
BaseView(isLoading: $viewModel.isLoading) {
@ -26,17 +29,17 @@ struct UserProfileFanTalkAllView: View {
HStack(spacing: 6.7) {
Text("응원")
.font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
Text("\(viewModel.cheersTotalCount)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
}
.padding(.top, 20)
Rectangle()
.frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5))
.foregroundColor(Color.gray90.opacity(0.5))
.padding(.top, 13.3)
HStack(spacing: 0) {
@ -44,8 +47,8 @@ struct UserProfileFanTalkAllView: View {
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.accentColor(Color(hex: "3bb9f1"))
.foregroundColor(Color.grayee)
.accentColor(Color.button)
.keyboardType(.default)
.padding(.horizontal, 13.3)
@ -61,18 +64,18 @@ struct UserProfileFanTalkAllView: View {
cheersContent = ""
}
}
.background(Color(hex: "232323"))
.background(Color.gray23)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "3bb9f1"))
.foregroundColor(Color.button)
)
.padding(.top, 13.3)
Rectangle()
.frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5))
.foregroundColor(Color.gray90.opacity(0.5))
.padding(.top, 13.3)
ScrollView(.vertical, showsIndicators: false) {
@ -96,6 +99,10 @@ struct UserProfileFanTalkAllView: View {
onClickDelete: { cheersId in
self.cheersId = cheersId
viewModel.isShowCheersDeleteView = true
},
onClickProfile: {
self.memberId = $0
self.isShowMemberProfilePopup = true
}
)
.onAppear {
@ -110,7 +117,7 @@ struct UserProfileFanTalkAllView: View {
} else {
Text("응원이 없습니다.\n\n처음으로 응원을 해보세요!")
.font(.custom(Font.light.rawValue, size: 13.3))
.foregroundColor(Color(hex: "bbbbbb"))
.foregroundColor(Color.graybb)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical, 60)
@ -136,7 +143,7 @@ struct UserProfileFanTalkAllView: View {
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff"))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
@ -171,6 +178,10 @@ struct UserProfileFanTalkAllView: View {
)
}
}
if isShowMemberProfilePopup {
MemberProfileDialog(isShowing: $isShowMemberProfilePopup, memberId: memberId)
}
}
}
}

View File

@ -16,6 +16,7 @@ struct UserProfileFanTalkCheersItemView: View {
let modifyCheer: (Int, String) -> Void
let reportPopup: (Int) -> Void
let onClickDelete: (Int) -> Void
let onClickProfile: (Int) -> Void
@State var replyContent: String = ""
@State var isShowInputReply = false
@ -34,6 +35,11 @@ struct UserProfileFanTalkCheersItemView: View {
.resizable()
.frame(width: 33.3, height: 33.3)
.clipShape(Circle())
.onTapGesture {
if UserDefaults.int(forKey: .userId) != cheersItem.memberId {
onClickProfile(cheersItem.memberId)
}
}
VStack(alignment: .leading, spacing: 0) {
Text("\(cheersItem.nickname)")
@ -51,10 +57,10 @@ struct UserProfileFanTalkCheersItemView: View {
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
.padding(13.3)
.background(Color(hex: "232323"))
.accentColor(Color(hex: "3bb9f1"))
.background(Color.gray23)
.accentColor(Color.button)
.keyboardType(.default)
.cornerRadius(10)
.overlay(
@ -65,7 +71,7 @@ struct UserProfileFanTalkCheersItemView: View {
Text("수정")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color(hex: "ffffff"))
.foregroundColor(Color.white)
.padding(13.3)
.background(Color.button)
.cornerRadius(6.7)
@ -78,7 +84,7 @@ struct UserProfileFanTalkCheersItemView: View {
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.button)
.padding(13.3)
.background(Color(hex: "222222"))
.background(Color.gray22)
.cornerRadius(6.7)
.onTapGesture {
isModeModify = false
@ -100,23 +106,23 @@ struct UserProfileFanTalkCheersItemView: View {
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
.padding(13.3)
.background(Color(hex: "232323"))
.accentColor(Color(hex: "3bb9f1"))
.background(Color.gray23)
.accentColor(Color.button)
.keyboardType(.default)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "3bb9f1"))
.foregroundColor(Color.button)
)
Text("등록")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color(hex: "ffffff"))
.foregroundColor(Color.white)
.padding(13.3)
.background(Color(hex: "3bb9f1"))
.background(Color.button)
.cornerRadius(6.7)
.onTapGesture {
if cheersItem.replyList.count > 0 {
@ -143,23 +149,24 @@ struct UserProfileFanTalkCheersItemView: View {
VStack(alignment: .leading, spacing: 8.3) {
Text(reply.content)
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "ffffff"))
.foregroundColor(Color.white)
.frame(minWidth: 100)
.padding(.horizontal, 6.7)
.padding(.vertical, 6.7)
.background(Color.button.opacity(0.3))
.cornerRadius(16.7)
.padding(.top, 18.3)
.fixedSize(horizontal: false, vertical: true)
HStack(spacing: 6.7) {
Text(reply.date)
.font(.custom(Font.medium.rawValue, size: 10.7))
.foregroundColor(Color(hex: "525252"))
.foregroundColor(Color.gray52)
if userId == UserDefaults.int(forKey: .userId) {
Text("답글 수정")
.font(.custom(Font.medium.rawValue, size: 10.7))
.foregroundColor(Color(hex: "9970ff"))
.foregroundColor(Color.button)
.onTapGesture {
self.replyContent = reply.content
isShowInputReply = true
@ -181,7 +188,7 @@ struct UserProfileFanTalkCheersItemView: View {
Rectangle()
.frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5))
.foregroundColor(Color.gray90.opacity(0.5))
.padding(.top, 13.3)
}
.frame(width: screenSize().width - 26.7)
@ -191,7 +198,7 @@ struct UserProfileFanTalkCheersItemView: View {
if cheersItem.memberId != UserDefaults.int(forKey: .userId) {
Text("신고하기")
.font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
.onTapGesture {
reportPopup(cheersItem.cheersId)
isShowPopupMenu = false
@ -201,7 +208,7 @@ struct UserProfileFanTalkCheersItemView: View {
if cheersItem.memberId == UserDefaults.int(forKey: .userId) {
Text("수정")
.font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
.onTapGesture {
isModeModify = true
isShowPopupMenu = false
@ -214,7 +221,7 @@ struct UserProfileFanTalkCheersItemView: View {
{
Text("삭제")
.font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
.onTapGesture {
onClickDelete(cheersItem.cheersId)
isShowPopupMenu = false
@ -222,7 +229,7 @@ struct UserProfileFanTalkCheersItemView: View {
}
}
.padding(10)
.background(Color(hex: "222222"))
.background(Color.gray22)
}
}
.contentShape(Rectangle())

View File

@ -17,6 +17,7 @@ struct UserProfileFanTalkView: View {
let errorPopup: (String) -> Void
let reportPopup: (Int) -> Void
let deletePopup: (Int) -> Void
let profilePopup: (Int) -> Void
@Binding var isLoading: Bool
@State private var cheersContent: String = ""
@ -26,13 +27,13 @@ struct UserProfileFanTalkView: View {
HStack(spacing: 0) {
Text("팬 Talk")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
Spacer()
Text("전체보기")
.font(.custom(Font.light.rawValue, size: 11.3))
.foregroundColor(Color(hex: "bbbbbb"))
.foregroundColor(Color.graybb)
.onTapGesture {
AppState.shared.setAppStep(step: .userProfileFanTalkAll(userId: userId))
}
@ -43,17 +44,17 @@ struct UserProfileFanTalkView: View {
HStack(spacing: 6.7) {
Text("응원")
.font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
Text("\(cheers.totalCount)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "777777"))
.foregroundColor(Color.gray77)
}
.padding(.top, 20)
Rectangle()
.frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5))
.foregroundColor(Color.gray90.opacity(0.5))
.padding(.top, 13.3)
HStack(spacing: 0) {
@ -61,8 +62,8 @@ struct UserProfileFanTalkView: View {
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.accentColor(Color(hex: "3bb9f1"))
.foregroundColor(Color.grayee)
.accentColor(Color.button)
.keyboardType(.default)
.padding(.horizontal, 13.3)
@ -78,18 +79,18 @@ struct UserProfileFanTalkView: View {
cheersContent = ""
}
}
.background(Color(hex: "232323"))
.background(Color.gray23)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "9970ff"))
.foregroundColor(Color.button)
)
.padding(.top, 13.3)
Rectangle()
.frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5))
.foregroundColor(Color.gray90.opacity(0.5))
.padding(.top, 13.3)
VStack(spacing: 20) {
@ -110,6 +111,9 @@ struct UserProfileFanTalkView: View {
},
onClickDelete: { cheersId in
deletePopup(cheersId)
},
onClickProfile: {
profilePopup($0)
}
)
.onTapGesture {
@ -119,7 +123,7 @@ struct UserProfileFanTalkView: View {
} else {
Text("응원이 없습니다.\n\n처음으로 응원을 해보세요!")
.font(.custom(Font.light.rawValue, size: 13.3))
.foregroundColor(Color(hex: "bbbbbb"))
.foregroundColor(Color.graybb)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical, 60)

View File

@ -81,6 +81,9 @@ struct GetAudioContentListItem: Decodable {
let isPin: Bool
let isAdult: Bool
let isScheduledToOpen: Bool
let isRented: Bool
let isOwned: Bool
let isSoldOut: Bool
}
struct GetCreatorActivitySummary: Decodable {

View File

@ -76,7 +76,10 @@ struct UserProfileContentView_Previews: PreviewProvider {
commentCount: 0,
isPin: true,
isAdult: false,
isScheduledToOpen: false
isScheduledToOpen: false,
isRented: false,
isOwned: false,
isSoldOut: false
)
]
)

View File

@ -12,6 +12,9 @@ struct UserProfileView: View {
let userId: Int
@StateObject var viewModel = UserProfileViewModel()
@State private var memberId: Int = 0
@State private var isShowMemberProfilePopup: Bool = false
var body: some View {
GeometryReader { proxy in
BaseView(isLoading: $viewModel.isLoading) {
@ -196,6 +199,10 @@ struct UserProfileView: View {
viewModel.cheersId = cheerId
viewModel.isShowCheersDeleteView = true
},
profilePopup: {
self.memberId = $0
self.isShowMemberProfilePopup = true
},
isLoading: $viewModel.isLoading
)
.padding(.top, 26.7)
@ -311,6 +318,10 @@ struct UserProfileView: View {
}
)
}
if isShowMemberProfilePopup {
MemberProfileDialog(isShowing: $isShowMemberProfilePopup, memberId: memberId)
}
}
}
.sheet(

View File

@ -14,36 +14,37 @@ struct FollowCreatorView: View {
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
DetailNavigationBar(title: "팔로잉 채널리스트")
DetailNavigationBar(title: "팔로잉 리스트")
HStack(spacing: 0) {
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
Text("\(viewModel.totalCount)")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "dd4500"))
.foregroundColor(Color.mainRed3)
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
Spacer()
}
.padding(.horizontal, 13.3)
.padding(.top, 6.7)
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 13.3) {
ForEach(0..<viewModel.creatorList.count, id: \.self) { index in
let creator = viewModel.creatorList[index]
FollowCreatorItemView(
creator: creator,
onClickFollow: { viewModel.creatorFollow(userId: $0) },
onClickUnFollow: { viewModel.creatorUnFollow(userId: $0) }
)
if viewModel.totalCount > 0 {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 13.3) {
ForEach(0..<viewModel.creatorList.count, id: \.self) { index in
let creator = viewModel.creatorList[index]
FollowCreatorItemView(
creator: creator,
onClickFollow: { viewModel.creatorFollow(userId: $0) },
onClickUnFollow: { viewModel.creatorUnFollow(userId: $0) }
)
.padding(.horizontal, 20)
.onTapGesture {
AppState.shared
@ -54,9 +55,15 @@ struct FollowCreatorView: View {
viewModel.getFollowedCreatorAllList()
}
}
}
}
.padding(.top, 13.3)
}
.padding(.top, 13.3)
} else {
Text("팔로우 중인 채널이 없습니다.")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
.frame(maxHeight: .infinity)
}
}
.onAppear {
@ -69,7 +76,7 @@ struct FollowCreatorView: View {
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "3bb9f1"))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)

View File

@ -73,8 +73,8 @@ final class LiveRepository {
return api.requestPublisher(.getRoomInfo(roomId: roomId))
}
func donation(roomId: Int, can: Int, message: String = "") -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.donation(request: LiveRoomDonationRequest(roomId: roomId, can: can, message: message)))
func donation(roomId: Int, can: Int, message: String = "", isSecret: Bool = false) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.donation(request: LiveRoomDonationRequest(roomId: roomId, can: can, message: message, isSecret: isSecret)))
}
func refundDonation(roomId: Int) -> AnyPublisher<Response, MoyaError> {

View File

@ -9,7 +9,7 @@ import Foundation
struct LiveRoomChatRawMessage: Codable {
enum LiveRoomChatRawMessageType: String, Codable {
case DONATION, EDIT_ROOM_INFO, SET_MANAGER, TOGGLE_ROULETTE, ROULETTE_DONATION
case DONATION, SECRET_DONATION, EDIT_ROOM_INFO, SET_MANAGER, TOGGLE_ROULETTE, ROULETTE_DONATION
}
let type: LiveRoomChatRawMessageType

View File

@ -43,7 +43,7 @@ struct LiveRoomDonationChatItemView: View {
.font(.system(size: 15))
.foregroundColor(Color(hex: "fdca2f"))
Text("을 후원하셨습니다.")
Text(chatMessage.chat.contains("비밀") ? "을 비밀후원하셨습니다.💰🪙" : "을 후원하셨습니다.💰🪙")
.font(.system(size: 15))
.foregroundColor(.white)
}
@ -58,6 +58,7 @@ struct LiveRoomDonationChatItemView: View {
.padding(13)
.frame(width: screenSize().width - 86, alignment: .leading)
.background(
chatMessage.chat.contains("비밀") ? Color(hex: "333333").opacity(0.8) :
chatMessage.can >= 10000 ? Color(hex: "c25264").opacity(0.8) :
chatMessage.can >= 5000 ? Color(hex: "d85e37").opacity(0.8) :
chatMessage.can >= 1000 ? Color(hex: "d38c38").opacity(0.8) :

View File

@ -9,8 +9,10 @@ import SwiftUI
struct LiveRoomDonationMessageDialog: View {
@StateObject var viewModel: LiveRoomViewModel
@Binding var isShowing: Bool
@StateObject var viewModel = LiveRoomViewModel()
@State var creatorId = 0
var body: some View {
ZStack {
@ -45,7 +47,7 @@ struct LiveRoomDonationMessageDialog: View {
ForEach(0..<viewModel.donationMessageList.count, id: \.self) { index in
let donationMessage = viewModel.donationMessageList[index]
LiveRoomDonationMessageItemView(message: donationMessage) {
LiveRoomDonationMessageItemView(message: donationMessage, creatorId: creatorId) {
viewModel.deleteDonationMessage(uuid: $0)
}
.onTapGesture {
@ -79,7 +81,7 @@ struct LiveRoomDonationMessageDialog: View {
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff"))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
@ -89,6 +91,7 @@ struct LiveRoomDonationMessageDialog: View {
}
.onAppear {
viewModel.getDonationMessageList()
creatorId = viewModel.liveRoomInfo?.creatorId ?? 0
}
}
}

View File

@ -10,6 +10,7 @@ import SwiftUI
struct LiveRoomDonationMessageItemView: View {
let message: LiveRoomDonationMessage
let creatorId: Int
let deleteDonationMessage: (String) -> Void
var body: some View {
@ -32,10 +33,12 @@ struct LiveRoomDonationMessageItemView: View {
Spacer()
Image("ic_close_white")
.resizable()
.frame(width: 13.3, height: 13.3)
.onTapGesture { deleteDonationMessage(message.uuid) }
if creatorId == UserDefaults.int(forKey: .userId) {
Image("ic_close_white")
.resizable()
.frame(width: 13.3, height: 13.3)
.onTapGesture { deleteDonationMessage(message.uuid) }
}
}
.padding(13.3)
.background(message.canMessage.trimmingCharacters(in: .whitespaces).isEmpty ? Color(hex: "c25264").opacity(0.8) : Color.gray33)
@ -51,6 +54,7 @@ struct LiveRoomDonationMessageItemView: View {
canMessage: "10캔을 후원하셨습니다",
donationMessage: "ㅅㅅㅅ"
),
creatorId: 0,
deleteDonationMessage: { _ in }
)
}
@ -63,6 +67,7 @@ struct LiveRoomDonationMessageItemView: View {
canMessage: "",
donationMessage: "[테스트] 당첨!"
),
creatorId: 0,
deleteDonationMessage: { _ in }
)
}

View File

@ -15,6 +15,7 @@ struct LiveRoomInfoEditDialog: View {
@State private var title = ""
@State private var notice = ""
@State private var isAdult = false
let placeholder = "라이브 공지를 입력하세요"
@ -23,24 +24,26 @@ struct LiveRoomInfoEditDialog: View {
let isLoading: Bool
let coverImageUrl: String?
let coverImage: UIImage?
var confirmAction: (String, String) -> Void
var confirmAction: (String, String, Bool) -> Void
init(
isShowing: Binding<Bool>,
isShowPhotoPicker: Binding<Bool>,
viewModel: LiveRoomViewModel,
isAdult: Bool,
isLoading: Bool,
currentTitle: String,
currentNotice: String,
coverImageUrl: String,
coverImage: UIImage?,
confirmAction: @escaping (String, String) -> Void
confirmAction: @escaping (String, String, Bool) -> Void
) {
self._isShowing = isShowing
self._isShowPhotoPicker = isShowPhotoPicker
self._viewModel = StateObject(wrappedValue: viewModel)
self.isLoading = isLoading
self.isAdult = isAdult
self.title = currentTitle
self.notice = currentNotice
@ -116,6 +119,30 @@ struct LiveRoomInfoEditDialog: View {
ContentInputView()
.padding(.top, 33.3)
if UserDefaults.bool(forKey: .auth) {
VStack(alignment: .leading, spacing: 8) {
Text("연령제한")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color.grayee)
HStack(spacing: 0) {
Text("19세 이상")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
Spacer()
Image(isAdult ? "btn_toggle_on_big" : "btn_toggle_off_big")
.resizable()
.frame(width: 33.3, height: 20)
.onTapGesture {
isAdult.toggle()
}
}
}
.padding(.top, 33.3)
}
LiveRoomMenuSelectView(
menu: $viewModel.menu,
isActivate: $viewModel.isActivateMenu,
@ -152,7 +179,8 @@ struct LiveRoomInfoEditDialog: View {
.onTapGesture {
confirmAction(
title,
notice.trimmingCharacters(in: .whitespacesAndNewlines) != placeholder ? notice : ""
notice.trimmingCharacters(in: .whitespacesAndNewlines) != placeholder ? notice : "",
isAdult
)
isShowing = false
}
@ -169,7 +197,7 @@ struct LiveRoomInfoEditDialog: View {
}
}
}
.background(Color(hex: "222222").edgesIgnoringSafeArea(.all))
.background(Color.gray22.edgesIgnoringSafeArea(.all))
if viewModel.isShowPopup {
LiveRoomDialogView(
@ -198,7 +226,7 @@ struct LiveRoomInfoEditDialog: View {
VStack(alignment: .leading, spacing: 0) {
Text("제목")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
TextField("라이브 제목을 입력하세요", text: $title)
.autocapitalization(.none)

View File

@ -148,7 +148,7 @@ struct LiveRoomProfilesDialogView: View {
role: listener.role,
onClickChangeListener: { _ in },
onClickInviteSpeaker: {
if viewModel.liveRoomInfo!.speakerList.count <= 4 {
if viewModel.liveRoomInfo!.speakerList.count <= 5 {
viewModel.inviteSpeaker(peerId: $0)
viewModel.popupContent = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요."
viewModel.isShowPopup = true

View File

@ -16,4 +16,5 @@ struct EditLiveRoomInfoRequest: Encodable {
var menuPanId: Int = 0
var menuPan: String = ""
var isActiveMenuPan: Bool? = nil
var isAdult: Bool? = nil
}

View File

@ -11,5 +11,6 @@ struct LiveRoomDonationRequest: Encodable {
let roomId: Int
let can: Int
let message: String
var isSecret: Bool = false
let container: String = "ios"
}

View File

@ -43,6 +43,13 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
@Published var isShowReportPopup = false
@Published var isShowErrorPopup = false
@Published var isShowUserProfilePopup = false
@Published var changeIsAdult = false {
didSet {
if changeIsAdult && !UserDefaults.bool(forKey: .auth) {
agora.speakerMute(true)
}
}
}
@Published var popupContent = ""
@Published var popupCancelTitle: String? = nil
@ -189,6 +196,34 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
var timer: DispatchSourceTimer?
private var blockedMemberIdList = Set<Int>()
func getBlockedMemberIdList() {
userRepository.getBlockedMemberIdList()
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { [unowned self] response in
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<[Int]>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.blockedMemberIdList.removeAll()
self.blockedMemberIdList.formUnion(data)
}
} catch {
}
}
.store(in: &subscription)
}
func setOriginOffset(_ offset: CGFloat) {
guard !isCheckedOriginOffset else { return }
self.originOffset = offset
@ -314,6 +349,10 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
getTotalDonationCan()
if data.isAdult && !UserDefaults.bool(forKey: .auth) {
changeIsAdult = true
}
if (userId > 0 && data.creatorId == UserDefaults.int(forKey: .userId)) {
let nickname = getUserNicknameAndProfileUrl(accountId: userId).nickname
onSuccess(nickname)
@ -380,11 +419,11 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
}
}
func donation(can: Int, message: String = "") {
func donation(can: Int, message: String = "", isSecret: Bool = false) {
if can > 0 {
isLoading = true
repository.donation(roomId: AppState.shared.roomId, can: can, message: message)
repository.donation(roomId: AppState.shared.roomId, can: can, message: message, isSecret: isSecret)
.sink { result in
switch result {
case .finished:
@ -402,9 +441,16 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
self.isLoading = false
if decoded.success {
let rawMessage = "\(can)캔을 후원하셨습니다."
var rawMessage = ""
if isSecret {
rawMessage = "\(can)캔을 비밀후원하셨습니다.💰🪙"
} else {
rawMessage = "\(can)캔을 후원하셨습니다.💰🪙"
}
let donationRawMessage = LiveRoomChatRawMessage(
type: .DONATION,
type: isSecret ? .SECRET_DONATION : .DONATION,
message: rawMessage,
can: can,
signature: decoded.data,
@ -414,36 +460,68 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
UserDefaults.set(UserDefaults.int(forKey: .can) - can, forKey: .can)
agora.sendRawMessageToGroup(
rawMessage: donationRawMessage,
completion: { [unowned self] errorCode in
if errorCode == .errorOk {
let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
self.messages.append(
LiveRoomDonationChat(
profileUrl: profileUrl,
nickname: nickname,
chat: rawMessage,
can: can,
donationMessage: message
if isSecret {
agora.sendRawMessageToPeer(
peerId: String(liveRoomInfo!.creatorId), rawMessage: donationRawMessage,
completion: { [unowned self] errorCode in
if errorCode == .ok {
let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
self.messages.append(
LiveRoomDonationChat(
profileUrl: profileUrl,
nickname: nickname,
chat: rawMessage,
can: can,
donationMessage: message
)
)
)
totalDonationCan += can
addSignature(signature: decoded.data)
self.messageChangeFlag.toggle()
if self.messages.count > 100 {
self.messages.remove(at: 0)
addSignature(signature: decoded.data)
self.messageChangeFlag.toggle()
if self.messages.count > 100 {
self.messages.remove(at: 0)
}
} else {
refundDonation()
}
} else {
},
fail: { [unowned self] in
refundDonation()
}
},
fail: { [unowned self] in
refundDonation()
}
)
)
} else {
agora.sendRawMessageToGroup(
rawMessage: donationRawMessage,
completion: { [unowned self] errorCode in
if errorCode == .errorOk {
let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
self.messages.append(
LiveRoomDonationChat(
profileUrl: profileUrl,
nickname: nickname,
chat: rawMessage,
can: can,
donationMessage: message
)
)
totalDonationCan += can
addSignature(signature: decoded.data)
self.messageChangeFlag.toggle()
if self.messages.count > 100 {
self.messages.remove(at: 0)
}
} else {
refundDonation()
}
},
fail: { [unowned self] in
refundDonation()
}
)
}
} else {
if let message = decoded.message {
self.popupContent = message
@ -640,7 +718,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
agora.sendMessageToPeer(peerId: peerId, rawMessage: LiveRoomRequestType.REQUEST_SPEAKER_ALLOW.rawValue.data(using: .utf8)!, completion: nil)
}
func editLiveRoomInfo(title: String, notice: String) {
func editLiveRoomInfo(title: String, notice: String, isAdult: Bool) {
let request = EditLiveRoomInfoRequest(
title: liveRoomInfo!.title != title ? title : nil,
notice: liveRoomInfo!.notice != notice ? notice : nil,
@ -649,10 +727,11 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
timezone: nil,
menuPanId: isActivateMenu ? menuId : 0,
menuPan: isActivateMenu ? menu : "",
isActiveMenuPan: isActivateMenu
isActiveMenuPan: isActivateMenu,
isAdult: liveRoomInfo!.isAdult != isAdult ? isAdult : nil
)
if (request.title == nil && request.notice == nil && coverImage == nil && menu == liveRoomInfo?.menuPan) {
if (request.title == nil && request.notice == nil && coverImage == nil && menu == liveRoomInfo?.menuPan && request.isAdult == nil) {
self.errorMessage = "변경사항이 없습니다."
self.isShowErrorPopup = true
return
@ -663,7 +742,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
let encoder = JSONEncoder()
encoder.outputFormatting = .withoutEscapingSlashes
if (request.title != nil || request.notice != nil || menu != liveRoomInfo?.menuPan) {
if (request.title != nil || request.notice != nil || request.isAdult != nil || menu != liveRoomInfo?.menuPan) {
let jsonData = try? encoder.encode(request)
if let jsonData = jsonData {
multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request"))
@ -814,22 +893,66 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
.store(in: &subscription)
}
func kickOut() {
repository.kickOut(roomId: AppState.shared.roomId, userId: kickOutId)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { _ in
}
.store(in: &subscription)
func shareRoom() {
guard let link = URL(string: "https://sodalive.net/?room_id=\(AppState.shared.roomId)") else { return }
let dynamicLinksDomainURIPrefix = "https://sodalive.page.link"
guard let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix) else {
self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
self.isShowErrorPopup = true
return
}
let nickname = getUserNicknameAndProfileUrl(accountId: kickOutId).nickname
linkBuilder.iOSParameters = DynamicLinkIOSParameters(bundleID: "kr.co.vividnext.sodalive")
linkBuilder.iOSParameters?.appStoreID = "6461721697"
linkBuilder.androidParameters = DynamicLinkAndroidParameters(packageName: "kr.co.vividnext.sodalive")
guard let longDynamicLink = linkBuilder.url else {
self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
self.isShowErrorPopup = true
return
}
DEBUG_LOG("The long URL is: \(longDynamicLink)")
DynamicLinkComponents.shortenURL(longDynamicLink, options: nil) { [unowned self] url, warnings, error in
let shortUrl = url?.absoluteString
if let liveRoomInfo = self.liveRoomInfo {
let urlString = shortUrl != nil ? shortUrl! : longDynamicLink.absoluteString
if liveRoomInfo.isPrivateRoom {
shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 소다라이브 비공개라이브에 초대하였습니다.\n" +
"※ 라이브 참여: \(urlString)\n" +
"(입장 비밀번호: \(liveRoomInfo.password!))"
} else {
shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 소다라이브 공개라이브에 초대하였습니다.\n" +
"※ 라이브 참여: \(urlString)"
}
isShowShareView = true
} else {
self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
self.isShowErrorPopup = true
return
}
}
}
func kickOut() {
if UserDefaults.int(forKey: .userId) == liveRoomInfo?.creatorId {
repository.kickOut(roomId: AppState.shared.roomId, userId: kickOutId)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { _ in
}
.store(in: &subscription)
let nickname = getUserNicknameAndProfileUrl(accountId: kickOutId).nickname
agora.sendMessageToPeer(peerId: String(kickOutId), rawMessage: LiveRoomRequestType.KICK_OUT.rawValue.data(using: .utf8)!, completion: { [unowned self] errorCode in
if errorCode == .ok {
self.popupContent = "\(nickname)님을 내보냈습니다."
@ -1248,6 +1371,8 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
}
func userBlock(onSuccess: @escaping (Int) -> Void) {
blockedMemberIdList.insert(reportUserId)
isLoading = true
userRepository.memberBlock(userId: reportUserId)
.sink { result in
@ -1291,6 +1416,8 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
}
func userUnBlock() {
blockedMemberIdList.remove(reportUserId)
isLoading = true
userRepository.memberUnBlock(userId: reportUserId)
.sink { result in
@ -1738,7 +1865,7 @@ extension LiveRoomViewModel: AgoraRtmDelegate {
self.popupConfirmTitle = "스피커로 초대"
self.popupConfirmAction = {
self.isShowPopup = false
if self.liveRoomInfo!.speakerList.count <= 4 {
if self.liveRoomInfo!.speakerList.count <= 5 {
self.requestSpeakerAllow(peerId)
} else {
self.errorMessage = "스피커 정원이 초과되었습니다."
@ -1814,6 +1941,31 @@ extension LiveRoomViewModel: AgoraRtmDelegate {
self.startNoChatting()
}
}
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(LiveRoomChatRawMessage.self, from: rawMessage.rawData)
let (nickname, profileUrl) = getUserNicknameAndProfileUrl(accountId: Int(peerId)!)
if decoded.type == .SECRET_DONATION {
self.messages.append(
LiveRoomDonationChat(
profileUrl: profileUrl,
nickname: nickname,
chat: decoded.message,
can: decoded.can,
donationMessage: decoded.donationMessage ?? ""
)
)
if let signature = decoded.signature {
self.addSignature(signature: signature)
} else if let imageUrl = decoded.signatureImageUrl {
self.addSignatureImage(imageUrl: imageUrl)
}
}
} catch {
}
}
}
}
@ -1864,11 +2016,12 @@ extension LiveRoomViewModel: AgoraRtmChannelDelegate {
} catch {
}
} else {
let memberId = Int(member.userId) ?? 0
let chat = message.text
let rank = getUserRank(userId: Int(member.userId) ?? 0)
let rank = getUserRank(userId: memberId)
if !chat.trimmingCharacters(in: .whitespaces).isEmpty {
messages.append(LiveRoomNormalChat(userId: Int(member.userId)!, profileUrl: profileUrl, nickname: nickname, rank: rank, chat: chat))
if !chat.trimmingCharacters(in: .whitespaces).isEmpty && !blockedMemberIdList.contains(memberId) {
messages.append(LiveRoomNormalChat(userId: memberId, profileUrl: profileUrl, nickname: nickname, rank: rank, chat: chat))
}
}

View File

@ -145,7 +145,7 @@ final class RouletteSettingsViewModel: ObservableObject {
}
}
if totalPercentage != Float(100) {
if totalPercentage > Float(100.1) || totalPercentage <= Float(99.99) {
isLoading = false
errorMessage = "확률이 100%가 아닙니다"
isShowErrorPopup = true

View File

@ -30,6 +30,7 @@ struct LiveRoomInfoGuestView: View {
let onClickQuit: () -> Void
let onClickToggleBg: () -> Void
let onClickShare: () -> Void
let onClickFollow: (Bool) -> Void
let onClickProfile: (Int) -> Void
let onClickNotice: () -> Void
@ -85,6 +86,13 @@ struct LiveRoomInfoGuestView: View {
strokeWidth: 1,
strokeCornerRadius: 5.3
) { onClickToggleBg() }
LiveRoomOverlayStrokeImageButton(
imageName: "ic_share",
strokeColor: Color.graybb,
strokeWidth: 1,
strokeCornerRadius: 5.3
) { onClickShare() }
}
HStack(spacing: 8) {
@ -211,6 +219,24 @@ struct LiveRoomInfoGuestView_Previews: PreviewProvider {
profileImage: "https://cf.sodalive.net/profile/4679/4679-profile-41e83399-234e-4541-8591-f961a025cfaa-5819-1699536915310",
role: .SPEAKER
),
LiveRoomMember(
id: 4,
nickname: "도화",
profileImage: "https://cf.sodalive.net/profile/26/26-profile-ddf78b4d-0300-4c50-9c84-5d8a95fd5fe2-4892-1705256364320",
role: .SPEAKER
),
LiveRoomMember(
id: 5,
nickname: "도화",
profileImage: "https://cf.sodalive.net/profile/26/26-profile-ddf78b4d-0300-4c50-9c84-5d8a95fd5fe2-4892-1705256364320",
role: .SPEAKER
),
LiveRoomMember(
id: 6,
nickname: "도화",
profileImage: "https://cf.sodalive.net/profile/26/26-profile-ddf78b4d-0300-4c50-9c84-5d8a95fd5fe2-4892-1705256364320",
role: .SPEAKER
),
],
muteSpeakerList: [],
activeSpeakerList: [],
@ -218,6 +244,7 @@ struct LiveRoomInfoGuestView_Previews: PreviewProvider {
isAdult: false,
onClickQuit: {},
onClickToggleBg: {},
onClickShare: {},
onClickFollow: { _ in },
onClickProfile: { _ in },
onClickNotice: {},

View File

@ -31,6 +31,7 @@ struct LiveRoomInfoHostView: View {
let onClickQuit: () -> Void
let onClickToggleBg: () -> Void
let onClickShare: () -> Void
let onClickEdit: () -> Void
let onClickProfile: (Int) -> Void
let onClickNotice: () -> Void
@ -77,6 +78,13 @@ struct LiveRoomInfoHostView: View {
strokeCornerRadius: 5.3
) { onClickToggleBg() }
LiveRoomOverlayStrokeImageButton(
imageName: "ic_share",
strokeColor: Color.graybb,
strokeWidth: 1,
strokeCornerRadius: 5.3
) { onClickShare() }
LiveRoomOverlayStrokeImageButton(
imageName: "ic_edit",
strokeColor: Color.graybb,
@ -227,12 +235,31 @@ struct LiveRoomInfoHostView_Previews: PreviewProvider {
profileImage: "https://cf.sodalive.net/profile/4679/4679-profile-41e83399-234e-4541-8591-f961a025cfaa-5819-1699536915310",
role: .SPEAKER
),
LiveRoomMember(
id: 4,
nickname: "도화",
profileImage: "https://cf.sodalive.net/profile/26/26-profile-ddf78b4d-0300-4c50-9c84-5d8a95fd5fe2-4892-1705256364320",
role: .SPEAKER
),
LiveRoomMember(
id: 5,
nickname: "도화",
profileImage: "https://cf.sodalive.net/profile/26/26-profile-ddf78b4d-0300-4c50-9c84-5d8a95fd5fe2-4892-1705256364320",
role: .SPEAKER
),
LiveRoomMember(
id: 6,
nickname: "도화",
profileImage: "https://cf.sodalive.net/profile/26/26-profile-ddf78b4d-0300-4c50-9c84-5d8a95fd5fe2-4892-1705256364320",
role: .SPEAKER
),
],
muteSpeakerList: [],
activeSpeakerList: [],
isAdult: false,
onClickQuit: {},
onClickToggleBg: {},
onClickShare: {},
onClickEdit: {},
onClickProfile: { _ in },
onClickNotice: {},

View File

@ -45,6 +45,9 @@ struct LiveRoomViewV2: View {
onClickToggleBg: {
viewModel.isBgOn.toggle()
},
onClickShare: {
viewModel.shareRoom()
},
onClickEdit: {
viewModel.isShowEditRoomInfoDialog = true
},
@ -92,6 +95,9 @@ struct LiveRoomViewV2: View {
onClickToggleBg: {
viewModel.isBgOn.toggle()
},
onClickShare: {
viewModel.shareRoom()
},
onClickFollow: {
if $0 {
viewModel.creatorUnFollow()
@ -145,16 +151,18 @@ struct LiveRoomViewV2: View {
ScrollView(.vertical, showsIndicators: false) {
scrollObservableView
LiveRoomChatView(messages: viewModel.messages) {
if $0 != UserDefaults.int(forKey: .userId) {
viewModel.getUserProfile(userId: $0)
if !viewModel.changeIsAdult || UserDefaults.bool(forKey: .auth) {
LiveRoomChatView(messages: viewModel.messages) {
if $0 != UserDefaults.int(forKey: .userId) {
viewModel.getUserProfile(userId: $0)
}
}
}
.frame(width: screenSize().width)
.rotationEffect(Angle(degrees: 180))
.valueChanged(value: viewModel.messageChangeFlag) { _ in
if viewModel.offset - viewModel.originOffset > (56.7 * 2) {
viewModel.isShowingNewChat = true
.frame(width: screenSize().width)
.rotationEffect(Angle(degrees: 180))
.valueChanged(value: viewModel.messageChangeFlag) { _ in
if viewModel.offset - viewModel.originOffset > (56.7 * 2) {
viewModel.isShowingNewChat = true
}
}
}
}
@ -197,13 +205,12 @@ struct LiveRoomViewV2: View {
)
}
if liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) &&
UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue {
LiveRoomRightBottomButton(
imageName: "ic_donation_message_list",
onClick: { viewModel.isShowDonationMessagePopup = true }
)
} else {
LiveRoomRightBottomButton(
imageName: "ic_donation_message_list",
onClick: { viewModel.isShowDonationMessagePopup = true }
)
if liveRoomInfo.creatorId != UserDefaults.int(forKey: .userId) {
LiveRoomRightBottomButton(
imageName: "ic_donation",
onClick: { viewModel.isShowDonationPopup = true }
@ -353,6 +360,7 @@ struct LiveRoomViewV2: View {
viewModel.getMemberCan()
viewModel.initAgoraEngine()
viewModel.getRoomInfo()
viewModel.getBlockedMemberIdList()
NotificationCenter.default.addObserver(
forName: UIApplication.willTerminateNotification,
@ -387,11 +395,22 @@ struct LiveRoomViewV2: View {
}
if viewModel.isShowDonationPopup {
LiveRoomDonationDialogView(isShowing: $viewModel.isShowDonationPopup, isAudioContentDonation: false) { can, message in
viewModel.donation(can: can, message: message)
LiveRoomDonationDialogView(isShowing: $viewModel.isShowDonationPopup, isAudioContentDonation: false) { can, message, isSecret in
viewModel.donation(can: can, message: message, isSecret: isSecret)
}
}
if viewModel.changeIsAdult && !UserDefaults.bool(forKey: .auth) {
SodaDialog(
title: "알림",
desc: "지금 참여하던 라이브는 '19세 이상' 연령제한이 설정되어 정보통신망 이용촉진 및 정보 보호 등에 관한 법률 및 청소년 보호법의 규정에 의해 만 19세 미만의 청소년은 이용할 수 없습니다.\n마이페이지에서 본인인증 후 다시 이용하시기 바랍니다.",
confirmButtonTitle: "확인",
confirmButtonAction: {
viewModel.quitRoom()
}
)
}
if viewModel.isShowQuitPopup {
SodaDialog(
title: "라이브 나가기",
@ -515,7 +534,7 @@ struct LiveRoomViewV2: View {
)
Rectangle()
.foregroundColor(Color(hex: "222222"))
.foregroundColor(Color.gray22)
.frame(width: screenSize().width, height: 15.3)
}
.ignoresSafeArea()
@ -640,15 +659,17 @@ struct LiveRoomViewV2: View {
isShowing: $viewModel.isShowEditRoomInfoDialog,
isShowPhotoPicker: $viewModel.isShowPhotoPicker,
viewModel: viewModel,
isAdult: liveRoomInfo.isAdult,
isLoading: viewModel.isLoading,
currentTitle: liveRoomInfo.title,
currentNotice: liveRoomInfo.notice,
coverImageUrl: liveRoomInfo.coverImageUrl,
coverImage: viewModel.coverImage
) { newTitle, newNotice in
) { newTitle, newNotice, isAdult in
self.viewModel.editLiveRoomInfo(
title: newTitle,
notice: newNotice
notice: newNotice,
isAdult: isAdult
)
}
} else {
@ -662,7 +683,7 @@ struct LiveRoomViewV2: View {
LiveRoomDonationRankingDialog(isShowing: $viewModel.isShowDonationRankingPopup)
}
.sheet(isPresented: $viewModel.isShowDonationMessagePopup) {
LiveRoomDonationMessageDialog(isShowing: $viewModel.isShowDonationMessagePopup)
LiveRoomDonationMessageDialog(viewModel: viewModel, isShowing: $viewModel.isShowDonationMessagePopup)
}
}
@ -674,7 +695,7 @@ struct LiveRoomViewV2: View {
}
private func inviteSpeaker(peerId: Int) {
if viewModel.liveRoomInfo!.speakerList.count <= 4 {
if viewModel.liveRoomInfo!.speakerList.count <= 5 {
viewModel.inviteSpeaker(peerId: peerId)
self.viewModel.popupContent = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요."
self.viewModel.isShowPopup = true

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