라이브 방 추가
|
@ -9,6 +9,24 @@
|
||||||
"version" : "1.2022062300.0"
|
"version" : "1.2022062300.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "agorartcengine_ios",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/AgoraIO/AgoraRtcEngine_iOS.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2e035dbfd39dea92ba9efd6447cd976fba85d5ff",
|
||||||
|
"version" : "4.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "agorartm_ios",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/AgoraIO/AgoraRtm_iOS",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "8d8d126da7e420798f39d1d95b6148eeb93971aa",
|
||||||
|
"version" : "1.4.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "alamofire",
|
"identity" : "alamofire",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "btn_bar_play.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 513 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "btn_bar_stop.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 294 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "btn_follow.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "btn_following.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/btn_following.imageset/btn_following.png
vendored
Normal file
After Width: | Height: | Size: 4.4 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_badge_manager.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_badge_manager.imageset/ic_badge_manager.png
vendored
Normal file
After Width: | Height: | Size: 2.0 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_bottom_white.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_bottom_white.imageset/ic_bottom_white.png
vendored
Normal file
After Width: | Height: | Size: 401 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_donation.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 4.8 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_donation_message_list.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_donation_message_list.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_donation_message_list.imageset/ic_donation_message_list.png
vendored
Normal file
After Width: | Height: | Size: 3.5 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_donation_status.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_donation_status.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_donation_status.imageset/ic_donation_status.png
vendored
Normal file
After Width: | Height: | Size: 6.7 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_edit.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 491 B |
21
SodaLive/Resources/Assets.xcassets/ic_live_detail_bottom.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_live_detail_bottom.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_live_detail_bottom.imageset/ic_live_detail_bottom.png
vendored
Normal file
After Width: | Height: | Size: 281 B |
21
SodaLive/Resources/Assets.xcassets/ic_live_detail_top.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_live_detail_top.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_live_detail_top.imageset/ic_live_detail_top.png
vendored
Normal file
After Width: | Height: | Size: 280 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_mic_off.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 645 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_mic_on.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 564 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_noti_pause.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_noti_pause.imageset/ic_noti_pause.png
vendored
Normal file
After Width: | Height: | Size: 442 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_noti_play.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 670 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_noti_stop.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 556 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_notice_normal.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_notice_normal.imageset/ic_notice_normal.png
vendored
Normal file
After Width: | Height: | Size: 2.6 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_notice_selected.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_notice_selected.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_notice_selected.imageset/ic_notice_selected.png
vendored
Normal file
After Width: | Height: | Size: 2.2 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_request_speak.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_request_speak.imageset/ic_request_speak.png
vendored
Normal file
After Width: | Height: | Size: 5.0 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_share.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 950 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_speaker_off.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_speaker_off.imageset/ic_speaker_off.png
vendored
Normal file
After Width: | Height: | Size: 5.1 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_speaker_on.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_speaker_on.imageset/ic_speaker_on.png
vendored
Normal file
After Width: | Height: | Size: 5.8 KiB |
|
@ -0,0 +1,140 @@
|
||||||
|
//
|
||||||
|
// Agora.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AgoraRtcKit
|
||||||
|
import AgoraRtmKit
|
||||||
|
|
||||||
|
final class Agora {
|
||||||
|
static let shared = Agora()
|
||||||
|
|
||||||
|
var rtcEngineDelegate: AgoraRtcEngineDelegate?
|
||||||
|
var rtmDelegate: AgoraRtmDelegate?
|
||||||
|
|
||||||
|
var rtcEngine: AgoraRtcEngineKit?
|
||||||
|
|
||||||
|
var rtmKit: AgoraRtmKit?
|
||||||
|
var rtmChannel: AgoraRtmChannel?
|
||||||
|
|
||||||
|
func initialize() {
|
||||||
|
rtcEngine = AgoraRtcEngineKit.sharedEngine(withAppId: AGORA_APP_ID, delegate: rtcEngineDelegate)
|
||||||
|
rtcEngine?.setChannelProfile(.liveBroadcasting)
|
||||||
|
rtcEngine?.enableAudio()
|
||||||
|
rtcEngine?.enableAudioVolumeIndication(500, smooth: 3, reportVad: true)
|
||||||
|
|
||||||
|
rtmKit = AgoraRtmKit(appId: AGORA_APP_ID, delegate: rtmDelegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deInit() {
|
||||||
|
if let rtcEngine = rtcEngine {
|
||||||
|
rtcEngine.leaveChannel(nil)
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
AgoraRtcEngineKit.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rtmChannel?.leave(completion: nil)
|
||||||
|
rtmKit?.logout(completion: nil)
|
||||||
|
rtcEngine = nil
|
||||||
|
rtmChannel = nil
|
||||||
|
rtmKit = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRole(role: AgoraClientRole) {
|
||||||
|
self.rtcEngine?.setClientRole(role)
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinChannel(
|
||||||
|
roomInfo: GetRoomInfoResponse,
|
||||||
|
rtmChannelDelegate: AgoraRtmChannelDelegate,
|
||||||
|
onConnectSuccess: @escaping (Bool) -> Void,
|
||||||
|
onConnectFail: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
if rtmChannel != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let userId = UserDefaults.int(forKey: .userId)
|
||||||
|
|
||||||
|
rtcEngine?.joinChannel(
|
||||||
|
byToken: roomInfo.rtcToken,
|
||||||
|
channelId: roomInfo.channelName,
|
||||||
|
info: nil,
|
||||||
|
uid: UInt(userId),
|
||||||
|
joinSuccess: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
rtcEngine?.setAudioProfile(.musicHighQualityStereo)
|
||||||
|
rtcEngine?.setAudioScenario(.gameStreaming)
|
||||||
|
|
||||||
|
rtmChannel = rtmKit?.createChannel(
|
||||||
|
withId: roomInfo.channelName,
|
||||||
|
delegate: rtmChannelDelegate
|
||||||
|
)
|
||||||
|
|
||||||
|
rtmKit?.login(
|
||||||
|
byToken: roomInfo.rtmToken,
|
||||||
|
user: String(userId),
|
||||||
|
completion: { [unowned self] loginErrorCode in
|
||||||
|
if loginErrorCode == .ok {
|
||||||
|
self.rtmChannel?.join(completion: { joinChannelErrorCode in
|
||||||
|
if joinChannelErrorCode == .channelErrorOk {
|
||||||
|
if userId == roomInfo.creatorId {
|
||||||
|
self.setRole(role: .broadcaster)
|
||||||
|
} else {
|
||||||
|
self.setRole(role: .audience)
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnectSuccess(userId == roomInfo.creatorId)
|
||||||
|
} else {
|
||||||
|
onConnectFail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
onConnectFail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMessageToPeer(peerId: String, rawMessage: Data, completion: AgoraRtmSendPeerMessageBlock?) {
|
||||||
|
let option = AgoraRtmSendMessageOptions()
|
||||||
|
option.enableOfflineMessaging = false
|
||||||
|
option.enableHistoricalMessaging = false
|
||||||
|
|
||||||
|
let message = AgoraRtmRawMessage(rawData: rawMessage, description: "")
|
||||||
|
rtmKit?.send(message, toPeer: peerId, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mute(_ isMute: Bool) {
|
||||||
|
rtcEngine?.muteLocalAudioStream(isMute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func speakerMute(_ isMute: Bool) {
|
||||||
|
rtcEngine?.muteAllRemoteAudioStreams(isMute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMessageToGroup(textMessage: String, completion: @escaping AgoraRtmSendChannelMessageBlock) {
|
||||||
|
let message = AgoraRtmMessage(text: textMessage)
|
||||||
|
rtmChannel?.send(message, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendRawMessageToGroup(rawMessage: LiveRoomChatRawMessage, completion: AgoraRtmSendChannelMessageBlock? = nil, fail: (() -> Void)? = nil) {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let jsonMessageData = try? encoder.encode(rawMessage)
|
||||||
|
|
||||||
|
if let jsonMessageData = jsonMessageData {
|
||||||
|
let message = AgoraRtmRawMessage(rawData: jsonMessageData, description: "")
|
||||||
|
rtmChannel?.send(message, completion: completion)
|
||||||
|
} else {
|
||||||
|
if let fail = fail {
|
||||||
|
fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ class AppState: ObservableObject {
|
||||||
@Published var isShowPlayer = false {
|
@Published var isShowPlayer = false {
|
||||||
didSet {
|
didSet {
|
||||||
if isShowPlayer {
|
if isShowPlayer {
|
||||||
|
ContentPlayManager.shared.stopAudio()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,8 @@ struct GetRoomListResponse: Decodable, Hashable {
|
||||||
let price: Int
|
let price: Int
|
||||||
let tags: [String]
|
let tags: [String]
|
||||||
let channelName: String?
|
let channelName: String?
|
||||||
let managerNickname: String
|
let creatorNickname: String
|
||||||
let managerId: Int
|
let creatorId: Int
|
||||||
let isReservation: Bool
|
let isReservation: Bool
|
||||||
let isPrivateRoom: Bool
|
let isPrivateRoom: Bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,19 @@ enum LiveApi {
|
||||||
case startLive(request: StartLiveRequest)
|
case startLive(request: StartLiveRequest)
|
||||||
case cancelRoom(request: CancelLiveRequest)
|
case cancelRoom(request: CancelLiveRequest)
|
||||||
case editLiveRoomInfo(roomId: Int, parameters: [MultipartFormData])
|
case editLiveRoomInfo(roomId: Int, parameters: [MultipartFormData])
|
||||||
|
case quitRoom(roomId: Int)
|
||||||
|
case getRoomInfo(roomId: Int)
|
||||||
|
case donation(request: LiveRoomDonationRequest)
|
||||||
|
case refundDonation(roomId: Int)
|
||||||
|
case setListener(request: SetManagerOrSpeakerOrAudienceRequest)
|
||||||
|
case setSpeaker(request: SetManagerOrSpeakerOrAudienceRequest)
|
||||||
|
case setManager(request: SetManagerOrSpeakerOrAudienceRequest)
|
||||||
|
case kickOut(request: LiveRoomKickOutRequest)
|
||||||
|
case donationStatus(roomId: Int)
|
||||||
|
case donationTotal(roomId: Int)
|
||||||
|
case getDonationMessageList(roomId: Int)
|
||||||
|
case deleteDonationMessage(roomId: Int, messageUUID: String)
|
||||||
|
case getUserProfile(roomId: Int, userId: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LiveApi: TargetType {
|
extension LiveApi: TargetType {
|
||||||
|
@ -73,19 +86,61 @@ extension LiveApi: TargetType {
|
||||||
|
|
||||||
case .editLiveRoomInfo(let roomId, _):
|
case .editLiveRoomInfo(let roomId, _):
|
||||||
return "/live/room/\(roomId)"
|
return "/live/room/\(roomId)"
|
||||||
|
|
||||||
|
case .quitRoom:
|
||||||
|
return "/live/room/quit"
|
||||||
|
|
||||||
|
case .getRoomInfo(let roomId):
|
||||||
|
return "/live/room/info/\(roomId)"
|
||||||
|
|
||||||
|
case .donation:
|
||||||
|
return "/live/room/donation"
|
||||||
|
|
||||||
|
case .refundDonation(let roomId):
|
||||||
|
return "/live/room/donation/refund/\(roomId)"
|
||||||
|
|
||||||
|
case .setListener:
|
||||||
|
return "/live/room/info/set/listener"
|
||||||
|
|
||||||
|
case .setSpeaker:
|
||||||
|
return "/live/room/info/set/speaker"
|
||||||
|
|
||||||
|
case .setManager:
|
||||||
|
return "/live/room/info/set/manager"
|
||||||
|
|
||||||
|
case .kickOut:
|
||||||
|
return "/live/room/kick-out"
|
||||||
|
|
||||||
|
case .donationStatus(let roomId):
|
||||||
|
return "/live/room/\(roomId)/donation-list"
|
||||||
|
|
||||||
|
case .donationTotal(let roomId):
|
||||||
|
return "/live/room/\(roomId)/donation-total"
|
||||||
|
|
||||||
|
case .getDonationMessageList:
|
||||||
|
return "/live/room/donation-message"
|
||||||
|
|
||||||
|
case .deleteDonationMessage:
|
||||||
|
return "/live/room/donation-message"
|
||||||
|
|
||||||
|
case .getUserProfile(let roomId, let userId):
|
||||||
|
return "/live/room/\(roomId)/profile/\(userId)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var method: Moya.Method {
|
var method: Moya.Method {
|
||||||
switch self {
|
switch self {
|
||||||
case .roomList, .recentVisitRoomUsers, .getReservations, .getReservation, .getRoomDetail, .getTags, .getRecentRoomInfo:
|
case .roomList, .recentVisitRoomUsers, .getReservations, .getReservation, .getRoomDetail, .getTags, .getRecentRoomInfo, .getRoomInfo, .donationStatus, .donationTotal, .getDonationMessageList, .getUserProfile:
|
||||||
return .get
|
return .get
|
||||||
|
|
||||||
case .makeReservation, .enterRoom, .createRoom:
|
case .makeReservation, .enterRoom, .createRoom, .quitRoom, .donation, .refundDonation, .kickOut:
|
||||||
return .post
|
return .post
|
||||||
|
|
||||||
case .cancelReservation, .startLive, .cancelRoom, .editLiveRoomInfo:
|
case .setListener, .setSpeaker, .setManager, .cancelReservation, .startLive, .cancelRoom, .editLiveRoomInfo:
|
||||||
return .put
|
return .put
|
||||||
|
|
||||||
|
case .deleteDonationMessage:
|
||||||
|
return .delete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +162,7 @@ extension LiveApi: TargetType {
|
||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
encoding: URLEncoding.queryString)
|
encoding: URLEncoding.queryString)
|
||||||
|
|
||||||
case .recentVisitRoomUsers, .getTags, .getRecentRoomInfo:
|
case .recentVisitRoomUsers, .getTags, .getRecentRoomInfo, .getRoomInfo, .refundDonation, .donationStatus, .donationTotal, .getUserProfile:
|
||||||
return .requestPlain
|
return .requestPlain
|
||||||
|
|
||||||
case .getReservations(let isActive):
|
case .getReservations(let isActive):
|
||||||
|
@ -153,6 +208,24 @@ extension LiveApi: TargetType {
|
||||||
|
|
||||||
case .editLiveRoomInfo(_, let parameters):
|
case .editLiveRoomInfo(_, let parameters):
|
||||||
return .uploadMultipart(parameters)
|
return .uploadMultipart(parameters)
|
||||||
|
|
||||||
|
case .quitRoom(let roomId):
|
||||||
|
return .requestParameters(parameters: ["id": roomId], encoding: URLEncoding.queryString)
|
||||||
|
|
||||||
|
case .donation(let request):
|
||||||
|
return .requestJSONEncodable(request)
|
||||||
|
|
||||||
|
case .setListener(let request), .setSpeaker(let request), .setManager(let request):
|
||||||
|
return .requestJSONEncodable(request)
|
||||||
|
|
||||||
|
case .kickOut(let request):
|
||||||
|
return .requestJSONEncodable(request)
|
||||||
|
|
||||||
|
case .getDonationMessageList(let roomId):
|
||||||
|
return .requestParameters(parameters: ["roomId": roomId], encoding: URLEncoding.queryString)
|
||||||
|
|
||||||
|
case .deleteDonationMessage(let roomId, let messageUUID):
|
||||||
|
return .requestJSONEncodable(DeleteLiveRoomDonationMessage(roomId: roomId, messageUUID: messageUUID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,4 +64,56 @@ final class LiveRepository {
|
||||||
func editLiveRoomInfo(roomId: Int, parameters: [MultipartFormData]) -> AnyPublisher<Response, MoyaError> {
|
func editLiveRoomInfo(roomId: Int, parameters: [MultipartFormData]) -> AnyPublisher<Response, MoyaError> {
|
||||||
return api.requestPublisher(.editLiveRoomInfo(roomId: roomId, parameters: parameters))
|
return api.requestPublisher(.editLiveRoomInfo(roomId: roomId, parameters: parameters))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func quitRoom(roomId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.quitRoom(roomId: roomId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRoomInfo(roomId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
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 refundDonation(roomId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.refundDonation(roomId: roomId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func setListener(roomId: Int, userId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.setListener(request: SetManagerOrSpeakerOrAudienceRequest(roomId: roomId, memberId: userId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSpeaker(roomId: Int, userId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.setSpeaker(request: SetManagerOrSpeakerOrAudienceRequest(roomId: roomId, memberId: userId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func setManager(roomId: Int, userId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
api.requestPublisher(.setManager(request: SetManagerOrSpeakerOrAudienceRequest(roomId: roomId, memberId: userId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func kickOut(roomId: Int, userId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.kickOut(request: LiveRoomKickOutRequest(roomId: roomId, userId: userId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func donationStatus(roomId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.donationStatus(roomId: roomId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTotalDoantionCan(roomId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.donationTotal(roomId: roomId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDonationMessageList(roomId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.getDonationMessageList(roomId: roomId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteDonationMessage(roomId: Int, messageUUID: String) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.deleteDonationMessage(roomId: roomId, messageUUID: messageUUID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserProfile(roomId: Int, userId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
api.requestPublisher(.getUserProfile(roomId: roomId, userId: userId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,9 @@ struct LiveView: View {
|
||||||
if viewModel.liveNowItems.count > 0 {
|
if viewModel.liveNowItems.count > 0 {
|
||||||
SectionLiveNowView(
|
SectionLiveNowView(
|
||||||
items: viewModel.liveNowItems,
|
items: viewModel.liveNowItems,
|
||||||
onClickParticipant: {_ in},
|
onClickParticipant: {
|
||||||
|
viewModel.enterRoom(roomId: $0)
|
||||||
|
},
|
||||||
onTapCreateLive: {
|
onTapCreateLive: {
|
||||||
AppState.shared.setAppStep(step: .createLive(timeSettingMode: .NOW, onSuccess: onCreateSuccess))
|
AppState.shared.setAppStep(step: .createLive(timeSettingMode: .NOW, onSuccess: onCreateSuccess))
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ struct LiveNowAllItemView: View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
HStack(alignment: .top, spacing: 0) {
|
HStack(alignment: .top, spacing: 0) {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
Text(item.managerNickname)
|
Text(item.creatorNickname)
|
||||||
.font(.custom(Font.medium.rawValue, size: 11.3))
|
.font(.custom(Font.medium.rawValue, size: 11.3))
|
||||||
.foregroundColor(Color(hex: "bbbbbb"))
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ struct LiveNowItemView: View {
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text("\(item.managerNickname)")
|
Text("\(item.creatorNickname)")
|
||||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
.foregroundColor(Color.white)
|
.foregroundColor(Color.white)
|
||||||
}
|
}
|
||||||
|
@ -101,8 +101,8 @@ struct LiveNowItemView_Previews: PreviewProvider {
|
||||||
price: 0,
|
price: 0,
|
||||||
tags: ["팬미팅", "힐링"],
|
tags: ["팬미팅", "힐링"],
|
||||||
channelName: nil,
|
channelName: nil,
|
||||||
managerNickname: "user8",
|
creatorNickname: "user8",
|
||||||
managerId: 19,
|
creatorId: 19,
|
||||||
isReservation: false,
|
isReservation: false,
|
||||||
isPrivateRoom: true
|
isPrivateRoom: true
|
||||||
)
|
)
|
||||||
|
|
|
@ -41,7 +41,7 @@ struct LiveReservationAllItemView: View {
|
||||||
.font(.custom(Font.medium.rawValue, size: 9.3))
|
.font(.custom(Font.medium.rawValue, size: 9.3))
|
||||||
.foregroundColor(Color(hex: "ffd300"))
|
.foregroundColor(Color(hex: "ffd300"))
|
||||||
|
|
||||||
Text(item.managerNickname)
|
Text(item.creatorNickname)
|
||||||
.font(.custom(Font.medium.rawValue, size: 11.3))
|
.font(.custom(Font.medium.rawValue, size: 11.3))
|
||||||
.foregroundColor(Color(hex: "bbbbbb"))
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
.padding(.top, 10)
|
.padding(.top, 10)
|
||||||
|
|
|
@ -41,7 +41,7 @@ struct LiveReservationItemView: View {
|
||||||
.font(.custom(Font.medium.rawValue, size: 9.3))
|
.font(.custom(Font.medium.rawValue, size: 9.3))
|
||||||
.foregroundColor(Color(hex: "ffd300"))
|
.foregroundColor(Color(hex: "ffd300"))
|
||||||
|
|
||||||
Text(item.managerNickname)
|
Text(item.creatorNickname)
|
||||||
.font(.custom(Font.medium.rawValue, size: 11.3))
|
.font(.custom(Font.medium.rawValue, size: 11.3))
|
||||||
.foregroundColor(Color(hex: "bbbbbb"))
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
.padding(.top, 10)
|
.padding(.top, 10)
|
||||||
|
@ -114,8 +114,8 @@ struct LiveReservationItemView_Previews: PreviewProvider {
|
||||||
price: 0,
|
price: 0,
|
||||||
tags: ["팬미팅", "힐링"],
|
tags: ["팬미팅", "힐링"],
|
||||||
channelName: nil,
|
channelName: nil,
|
||||||
managerNickname: "user8",
|
creatorNickname: "user8",
|
||||||
managerId: 19,
|
creatorId: 19,
|
||||||
isReservation: false,
|
isReservation: false,
|
||||||
isPrivateRoom: true
|
isPrivateRoom: true
|
||||||
))
|
))
|
||||||
|
|
|
@ -53,7 +53,7 @@ struct MyLiveReservationItemView: View {
|
||||||
.font(.custom(Font.medium.rawValue, size: 9.3))
|
.font(.custom(Font.medium.rawValue, size: 9.3))
|
||||||
.foregroundColor(Color(hex: "ffd300"))
|
.foregroundColor(Color(hex: "ffd300"))
|
||||||
|
|
||||||
Text(item.managerNickname)
|
Text(item.creatorNickname)
|
||||||
.font(.custom(Font.medium.rawValue, size: 11.3))
|
.font(.custom(Font.medium.rawValue, size: 11.3))
|
||||||
.foregroundColor(Color(hex: "bbbbbb"))
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
.padding(.top, 10)
|
.padding(.top, 10)
|
||||||
|
@ -103,8 +103,8 @@ struct MyLiveReservationItemView_Previews: PreviewProvider {
|
||||||
price: 0,
|
price: 0,
|
||||||
tags: ["팬미팅", "힐링"],
|
tags: ["팬미팅", "힐링"],
|
||||||
channelName: nil,
|
channelName: nil,
|
||||||
managerNickname: "user8",
|
creatorNickname: "user8",
|
||||||
managerId: 19,
|
creatorId: 19,
|
||||||
isReservation: false,
|
isReservation: false,
|
||||||
isPrivateRoom: true
|
isPrivateRoom: true
|
||||||
),
|
),
|
||||||
|
|
|
@ -53,7 +53,7 @@ struct SectionLiveReservationView: View {
|
||||||
ForEach(0..<items.count, id: \.self) { index in
|
ForEach(0..<items.count, id: \.self) { index in
|
||||||
let item = items[index]
|
let item = items[index]
|
||||||
|
|
||||||
if item.managerId == UserDefaults.int(forKey: .userId) {
|
if item.creatorId == UserDefaults.int(forKey: .userId) {
|
||||||
MyLiveReservationItemView(item: item, index: index)
|
MyLiveReservationItemView(item: item, index: index)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
//
|
||||||
|
// LiveRoomChat.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum LiveRoomChatType: String {
|
||||||
|
case CHAT, DONATION, JOIN
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol LiveRoomChat {
|
||||||
|
var type: LiveRoomChatType { get set }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveRoomNormalChat: LiveRoomChat {
|
||||||
|
let userId: Int
|
||||||
|
let profileUrl: String
|
||||||
|
let nickname: String
|
||||||
|
let rank: Int
|
||||||
|
let chat: String
|
||||||
|
|
||||||
|
var type: LiveRoomChatType = .CHAT
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveRoomDonationChat: LiveRoomChat {
|
||||||
|
let profileUrl: String
|
||||||
|
let nickname: String
|
||||||
|
let chat: String
|
||||||
|
let can: Int
|
||||||
|
let donationMessage: String
|
||||||
|
|
||||||
|
var type: LiveRoomChatType = .DONATION
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveRoomJoinChat: LiveRoomChat {
|
||||||
|
let nickname: String
|
||||||
|
|
||||||
|
var type: LiveRoomChatType = .JOIN
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
//
|
||||||
|
// LiveRoomChatItemView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct LiveRoomChatItemView: View {
|
||||||
|
|
||||||
|
let chatMessage: LiveRoomNormalChat
|
||||||
|
let onClickProfile: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top, spacing: 13.3) {
|
||||||
|
ZStack {
|
||||||
|
switch chatMessage.rank + 1 {
|
||||||
|
case -2:
|
||||||
|
Color(hex: "4999e3")
|
||||||
|
.frame(width: 33.3, height: 33.3, alignment: .top)
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
case -1:
|
||||||
|
Color(hex: "6f3dec")
|
||||||
|
.frame(width: 33.3, height: 33.3, alignment: .top)
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
Color(hex: "fdca2f")
|
||||||
|
.frame(width: 33.3, height: 33.3, alignment: .top)
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
Color(hex: "dcdcdc")
|
||||||
|
.frame(width: 33.3, height: 33.3, alignment: .top)
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
Color(hex: "c67e4a")
|
||||||
|
.frame(width: 33.3, height: 33.3, alignment: .top)
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
default:
|
||||||
|
Color(hex: "bbbbbb")
|
||||||
|
.frame(width: 33.3, height: 33.3, alignment: .top)
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
|
||||||
|
KFImage(URL(string: chatMessage.profileUrl))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 30, height: 30, alignment: .top)
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
VStack(alignment: .trailing, spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
switch chatMessage.rank + 1 {
|
||||||
|
case -2:
|
||||||
|
Image("ic_badge_manager")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 16.7, height: 16.7)
|
||||||
|
|
||||||
|
case -1:
|
||||||
|
Image("ic_crown")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 16.7, height: 16.7)
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
Image("ic_crown_1")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 16.7, height: 16.7)
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
Image("ic_crown_2")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 16.7, height: 16.7)
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
Image("ic_crown_3")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 16.7, height: 16.7)
|
||||||
|
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 33.3, height: 33.3, alignment: .trailing)
|
||||||
|
}
|
||||||
|
.onTapGesture { onClickProfile() }
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 6.7) {
|
||||||
|
HStack(spacing: 5) {
|
||||||
|
if chatMessage.rank == -3 {
|
||||||
|
Text("스탭")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(2)
|
||||||
|
.background(Color(hex: "4999e3"))
|
||||||
|
.cornerRadius(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(chatMessage.nickname)
|
||||||
|
.font(.custom(Font.light.rawValue, size: 12))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(chatMessage.chat)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.lineLimit(nil)
|
||||||
|
.lineSpacing(6)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 5.3)
|
||||||
|
.background(
|
||||||
|
UserDefaults.int(forKey: .userId) == chatMessage.userId ?
|
||||||
|
Color(hex: "9970ff").opacity(0.6) :
|
||||||
|
Color.black.opacity(0.6)
|
||||||
|
)
|
||||||
|
.cornerRadius(3.3)
|
||||||
|
}
|
||||||
|
.frame(width: screenSize().width - 86, alignment: .leading)
|
||||||
|
.padding(.leading, 20)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
// LiveRoomChatRawMessage.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct LiveRoomChatRawMessage: Codable {
|
||||||
|
enum LiveRoomChatRawMessageType: String, Codable {
|
||||||
|
case DONATION, EDIT_ROOM_INFO, SET_MANAGER
|
||||||
|
}
|
||||||
|
|
||||||
|
let type: LiveRoomChatRawMessageType
|
||||||
|
let message: String
|
||||||
|
let can: Int
|
||||||
|
let donationMessage: String?
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
//
|
||||||
|
// LiveRoomDonationChatItemView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct LiveRoomDonationChatItemView: View {
|
||||||
|
|
||||||
|
let chatMessage: LiveRoomDonationChat
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
ZStack(alignment: .bottomTrailing) {
|
||||||
|
KFImage(URL(string: chatMessage.profileUrl))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 33.3, height: 33.3, alignment: .top)
|
||||||
|
.clipped()
|
||||||
|
.cornerRadius(23.3)
|
||||||
|
|
||||||
|
Image("ic_can")
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 6.7) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text(chatMessage.nickname)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
Text("님이")
|
||||||
|
.font(.custom(Font.light.rawValue, size: 12))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("\(chatMessage.can)캔")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13))
|
||||||
|
.foregroundColor(Color(hex: "fdca2f"))
|
||||||
|
|
||||||
|
Text("을 후원하셨습니다.")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !chatMessage.donationMessage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
Text("\"\(chatMessage.donationMessage)\"")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(13)
|
||||||
|
.frame(width: screenSize().width - 86, alignment: .leading)
|
||||||
|
.background(
|
||||||
|
chatMessage.can >= 100000 ? Color(hex: "c25264") :
|
||||||
|
chatMessage.can >= 50000 ? Color(hex: "d85e37").opacity(0.9) :
|
||||||
|
chatMessage.can >= 10000 ? Color(hex: "d38c38").opacity(0.9) :
|
||||||
|
chatMessage.can >= 5000 ? Color(hex: "59548f").opacity(0.9) :
|
||||||
|
chatMessage.can >= 1000 ? Color(hex: "4d6aa4").opacity(0.9) :
|
||||||
|
chatMessage.can >= 500 ? Color(hex: "2d7390").opacity(0.9) :
|
||||||
|
Color(hex: "548f7d").opacity(0.9)
|
||||||
|
)
|
||||||
|
.cornerRadius(10)
|
||||||
|
.padding(.leading, 20)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
//
|
||||||
|
// LiveRoomJoinChatItemView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LiveRoomJoinChatItemView: View {
|
||||||
|
|
||||||
|
let chatMessage: LiveRoomJoinChat
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("'")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Text(chatMessage.nickname)
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "ffdc00"))
|
||||||
|
|
||||||
|
Text("'님이 입장하셨습니다.")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
}
|
||||||
|
.padding(.vertical, 6.7)
|
||||||
|
.frame(width: screenSize().width - 86)
|
||||||
|
.background(Color(hex: "3d2a6c"))
|
||||||
|
.cornerRadius(4.7)
|
||||||
|
.padding(.leading, 20)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// DeleteLiveRoomDonationMessage.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct DeleteLiveRoomDonationMessage: Encodable {
|
||||||
|
let roomId: Int
|
||||||
|
let messageUUID: String
|
||||||
|
}
|
|
@ -485,7 +485,7 @@ struct LiveDetailView: View {
|
||||||
|
|
||||||
if room.numberOfParticipants > 0 {
|
if room.numberOfParticipants > 0 {
|
||||||
HStack(spacing: 6.7) {
|
HStack(spacing: 6.7) {
|
||||||
Image(isExpandParticipantArea ? "ic_suda_detail_top" : "ic_suda_detail_bottom")
|
Image(isExpandParticipantArea ? "ic_live_detail_top" : "ic_live_detail_bottom")
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
//
|
||||||
|
// LiveRoomDialogView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LiveRoomDialogView: View {
|
||||||
|
|
||||||
|
let content: String
|
||||||
|
|
||||||
|
let cancelTitle: String?
|
||||||
|
let cancelAction: (() -> Void)?
|
||||||
|
|
||||||
|
let confirmTitle: String?
|
||||||
|
let confirmAction: (() -> Void)?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
HStack(alignment: .center, spacing: 11.7) {
|
||||||
|
Image("ic_request_speak")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 36, height: 36)
|
||||||
|
|
||||||
|
Text(content)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 26.7)
|
||||||
|
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if let cancelTitle = cancelTitle, let cancelAction = cancelAction {
|
||||||
|
Text(cancelTitle)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 8.3)
|
||||||
|
.background(Color.white.opacity(0.2))
|
||||||
|
.cornerRadius(13.3)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 13.3)
|
||||||
|
.strokeBorder()
|
||||||
|
.foregroundColor(.white)
|
||||||
|
)
|
||||||
|
.onTapGesture { cancelAction() }
|
||||||
|
}
|
||||||
|
|
||||||
|
if let confirmTitle = confirmTitle, let confirmAction = confirmAction {
|
||||||
|
Text(confirmTitle)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 8.3)
|
||||||
|
.background(Color.white)
|
||||||
|
.cornerRadius(13.3)
|
||||||
|
.onTapGesture { confirmAction() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 26.7)
|
||||||
|
.padding(.top, confirmTitle != nil || cancelTitle != nil ? 10 : 0)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 20)
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.cornerRadius(16.7)
|
||||||
|
.padding(.horizontal, 26.7)
|
||||||
|
.frame(width: screenSize().width)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
//
|
||||||
|
// LiveRoomDonationMessageDialog.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LiveRoomDonationMessageDialog: View {
|
||||||
|
|
||||||
|
@Binding var isShowing: Bool
|
||||||
|
@StateObject var viewModel = LiveRoomViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Color.black
|
||||||
|
.opacity(0.7)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onTapGesture {
|
||||||
|
hideKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("후원메시지")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 14.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Text("(\(viewModel.donationMessageCount))")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("닫기")
|
||||||
|
.font(.custom(Font.light.rawValue, size: 14.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.onTapGesture { isShowing = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
if viewModel.donationMessageList.count > 0 {
|
||||||
|
LazyVStack(spacing: 10.7) {
|
||||||
|
ForEach(0..<viewModel.donationMessageList.count, id: \.self) { index in
|
||||||
|
let donationMessage = viewModel.donationMessageList[index]
|
||||||
|
|
||||||
|
HStack(alignment: .top, spacing: 0) {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("\(donationMessage.nickname)님이")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
Text("\(donationMessage.canMessage)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
Text("'\(donationMessage.donationMessage)'")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("ic_close_white")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 13.3, height: 13.3)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.deleteDonationMessage(uuid: donationMessage.uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(13.3)
|
||||||
|
.background(Color(hex: "333333"))
|
||||||
|
.cornerRadius(5.3)
|
||||||
|
.onTapGesture {
|
||||||
|
UIPasteboard.general.string = donationMessage.donationMessage
|
||||||
|
self.viewModel.errorMessage = "후원 메시지가 복사되었습니다."
|
||||||
|
self.viewModel.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 18.7)
|
||||||
|
} else {
|
||||||
|
Text("후원메시지가 없습니다.")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.padding(.top, 30)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
.cornerRadius(8)
|
||||||
|
|
||||||
|
if viewModel.isLoading {
|
||||||
|
LoadingView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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(hex: "9970ff"))
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.cornerRadius(20)
|
||||||
|
.padding(.bottom, 66.7)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
viewModel.getDonationMessageList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
//
|
||||||
|
// LiveRoomDonationRankingDialog.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LiveRoomDonationRankingDialog: View {
|
||||||
|
|
||||||
|
@Binding var isShowing: Bool
|
||||||
|
@StateObject var viewModel = LiveRoomViewModel()
|
||||||
|
|
||||||
|
let columns = [GridItem(.flexible())]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("현재 라이브 후원랭킹")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 14.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("ic_close_white")
|
||||||
|
.onTapGesture { isShowing = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
if let donationStatus = viewModel.donationStatus {
|
||||||
|
LiveRoomDonationRankingTotalCanView(totalCan: donationStatus.totalCan)
|
||||||
|
.padding(.top, 25)
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("전체")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Text("\(donationStatus.totalCount)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.padding(.leading, 6.7)
|
||||||
|
|
||||||
|
Text("명")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "777777"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.top, 13.3)
|
||||||
|
|
||||||
|
ScrollView(showsIndicators: false) {
|
||||||
|
LazyVGrid(columns: columns, spacing: 0) {
|
||||||
|
ForEach(0..<donationStatus.donationList.count, id: \.self) { index in
|
||||||
|
let item = donationStatus.donationList[index]
|
||||||
|
LiveRoomDonationRankingItemView(
|
||||||
|
index: index,
|
||||||
|
item: item,
|
||||||
|
itemCount: donationStatus.donationList.count
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
.cornerRadius(8)
|
||||||
|
|
||||||
|
if viewModel.isLoading {
|
||||||
|
LoadingView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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(hex: "9970ff"))
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.cornerRadius(20)
|
||||||
|
.padding(.bottom, 66.7)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
viewModel.getDonationStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
//
|
||||||
|
// LiveRoomDonationRankingItemView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct LiveRoomDonationRankingItemView: View {
|
||||||
|
|
||||||
|
let index: Int
|
||||||
|
let item: GetLiveRoomDonationItem
|
||||||
|
let itemCount: Int
|
||||||
|
|
||||||
|
let rankingCrawns = ["ic_crown_1", "ic_crown_2", "ic_crown_3"]
|
||||||
|
let rankingColors = [
|
||||||
|
[Color(hex: "ffdc00"), Color(hex: "ffb600")],
|
||||||
|
[Color(hex: "ffffff"), Color(hex: "9f9f9f")],
|
||||||
|
[Color(hex: "e6a77a"), Color(hex: "c67e4a")],
|
||||||
|
[Color(hex: "ffffff").opacity(0), Color(hex: "ffffff").opacity(0)]
|
||||||
|
]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
ZStack {
|
||||||
|
KFImage(URL(string: item.profileImage))
|
||||||
|
.cancelOnDisappear(true)
|
||||||
|
.downsampling(size: CGSize(width: 60, height: 60))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 60, height: 60, alignment: .top)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.overlay(
|
||||||
|
Circle()
|
||||||
|
.stroke(
|
||||||
|
AngularGradient(colors: rankingColors[index < 4 ? index : 3], center: .center),
|
||||||
|
lineWidth: 3
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if index < 3 {
|
||||||
|
VStack(alignment: .trailing, spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(rankingCrawns[index])
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 25, height: 25)
|
||||||
|
}
|
||||||
|
.frame(width: 63, height: 63, alignment: .trailing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 63, height: 63)
|
||||||
|
|
||||||
|
Text("\(index + 1)")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.padding(.leading, 20)
|
||||||
|
.padding(.trailing, 13.3)
|
||||||
|
|
||||||
|
let nickname = item.nickname.count > 10 ? "\(String(item.nickname.prefix(10)))..." : item.nickname
|
||||||
|
Text(nickname)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if item.can > 0 {
|
||||||
|
Text("\(item.can) 코인")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, isTop3Index(index: index) ? 20 : 0)
|
||||||
|
.padding(.top, getTopPadding(index: index))
|
||||||
|
.padding(.bottom, getBottomPadding(index: index))
|
||||||
|
.background(Color(hex: "2b2635").opacity(isTop3Index(index: index) ? 1 : 0))
|
||||||
|
.cornerRadius(4.7, corners: cornerRadiusConers(index: index))
|
||||||
|
.padding(.horizontal, isTop3Index(index: index) ? 0 : 6.7)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isTop3Index(index: Int) -> Bool {
|
||||||
|
return index == 0 || index == 1 || index == 2
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getTopPadding(index: Int) -> CGFloat {
|
||||||
|
if index == 0 || index == 3 {
|
||||||
|
return 20
|
||||||
|
} else {
|
||||||
|
return 6.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getBottomPadding(index: Int) -> CGFloat {
|
||||||
|
if (index == 0 && itemCount == 1) || (index == 1 && itemCount == 2) || index == 2 {
|
||||||
|
return 20
|
||||||
|
} else {
|
||||||
|
return 6.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cornerRadiusConers(index: Int) -> UIRectCorner {
|
||||||
|
if (index == 0 && itemCount == 1) {
|
||||||
|
return [.topLeft, .topRight, .bottomLeft, .bottomRight]
|
||||||
|
} else if index == 0 {
|
||||||
|
return [.topLeft, .topRight]
|
||||||
|
} else if (index == 1 && itemCount == 2) || index == 2 {
|
||||||
|
return [.bottomLeft, .bottomRight]
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
//
|
||||||
|
// LiveRoomDonationRankingTotalCanView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LiveRoomDonationRankingTotalCanView: View {
|
||||||
|
|
||||||
|
let totalCan: Int
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .center, spacing: 0) {
|
||||||
|
Text("합계")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "d2d2d2"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("\(totalCan)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 16))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
|
||||||
|
Text("캔")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10.7))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
.padding(.leading, 4)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 18.7)
|
||||||
|
.padding(.vertical, 10.7)
|
||||||
|
.background(Color(hex: "2b2635"))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,238 @@
|
||||||
|
//
|
||||||
|
// LiveRoomInfoEditDialog.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct LiveRoomInfoEditDialog: View {
|
||||||
|
|
||||||
|
@Binding var isShowing: Bool
|
||||||
|
@Binding var isShowPhotoPicker: Bool
|
||||||
|
|
||||||
|
@State private var title = ""
|
||||||
|
@State private var notice = ""
|
||||||
|
|
||||||
|
let placeholder = "라이브 공지를 입력하세요"
|
||||||
|
|
||||||
|
let viewModel: LiveRoomViewModel
|
||||||
|
|
||||||
|
let isLoading: Bool
|
||||||
|
let coverImageUrl: String?
|
||||||
|
let coverImage: UIImage?
|
||||||
|
var confirmAction: (String, String) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
isShowing: Binding<Bool>,
|
||||||
|
isShowPhotoPicker: Binding<Bool>,
|
||||||
|
viewModel: LiveRoomViewModel,
|
||||||
|
isLoading: Bool,
|
||||||
|
currentTitle: String,
|
||||||
|
currentNotice: String,
|
||||||
|
coverImageUrl: String,
|
||||||
|
coverImage: UIImage?,
|
||||||
|
confirmAction: @escaping (String, String) -> Void
|
||||||
|
) {
|
||||||
|
self._isShowing = isShowing
|
||||||
|
self._isShowPhotoPicker = isShowPhotoPicker
|
||||||
|
|
||||||
|
self.viewModel = viewModel
|
||||||
|
self.isLoading = isLoading
|
||||||
|
|
||||||
|
self.title = currentTitle
|
||||||
|
self.notice = currentNotice
|
||||||
|
self.coverImageUrl = coverImageUrl
|
||||||
|
self.coverImage = coverImage
|
||||||
|
self.confirmAction = confirmAction
|
||||||
|
|
||||||
|
UITextView.appearance().backgroundColor = .clear
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("라이브 수정")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("ic_close_white")
|
||||||
|
.onTapGesture {
|
||||||
|
isShowing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
ZStack {
|
||||||
|
if let coverImage = coverImage {
|
||||||
|
Image(uiImage: coverImage)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 80, height: 116.8, alignment: .top)
|
||||||
|
.clipped()
|
||||||
|
.cornerRadius(10)
|
||||||
|
} else if let coverImageUrl = coverImageUrl {
|
||||||
|
KFImage(URL(string: coverImageUrl))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 80, height: 116.8, alignment: .top)
|
||||||
|
.clipped()
|
||||||
|
.cornerRadius(10)
|
||||||
|
} else {
|
||||||
|
Image("ic_logo_220")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 80, height: 116.8)
|
||||||
|
.background(Color(hex: "3e3358"))
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image("ic_camera")
|
||||||
|
.padding(10)
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.cornerRadius(30)
|
||||||
|
.offset(x: 40, y: 40)
|
||||||
|
}
|
||||||
|
.frame(alignment: .bottomTrailing)
|
||||||
|
.padding(.top, 13.3)
|
||||||
|
.onTapGesture {
|
||||||
|
isShowPhotoPicker = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
TitleInputView()
|
||||||
|
.padding(.top, 40)
|
||||||
|
|
||||||
|
ContentInputView()
|
||||||
|
.padding(.top, 33.3)
|
||||||
|
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
Text("취소")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.padding(.vertical, 16)
|
||||||
|
.frame(width: (screenSize().width - 40) / 2)
|
||||||
|
.background(Color(hex: "9970ff").opacity(0.2))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.strokeBorder(lineWidth: 1)
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
isShowing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("수정하기")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.vertical, 16)
|
||||||
|
.frame(width: (screenSize().width - 40) / 2)
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.onTapGesture {
|
||||||
|
confirmAction(
|
||||||
|
title,
|
||||||
|
notice.trimmingCharacters(in: .whitespacesAndNewlines) != placeholder ? notice : ""
|
||||||
|
)
|
||||||
|
isShowing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 45)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(13.3)
|
||||||
|
.frame(width: proxy.size.width, height: proxy.size.height)
|
||||||
|
.onTapGesture { hideKeyboard() }
|
||||||
|
}
|
||||||
|
.background(Color(hex: "222222").edgesIgnoringSafeArea(.all))
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isShowPopup {
|
||||||
|
LiveRoomDialogView(
|
||||||
|
content: viewModel.errorMessage,
|
||||||
|
cancelTitle: viewModel.popupCancelTitle,
|
||||||
|
cancelAction: viewModel.popupCancelAction,
|
||||||
|
confirmTitle: viewModel.popupConfirmTitle,
|
||||||
|
confirmAction: viewModel.popupConfirmAction
|
||||||
|
).onAppear {
|
||||||
|
if viewModel.popupConfirmTitle == nil && viewModel.popupConfirmAction == nil {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||||
|
viewModel.isShowPopup = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isLoading {
|
||||||
|
LoadingView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func TitleInputView() -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
Text("제목")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
TextField("라이브 제목을 입력하세요", text: $title)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.accentColor(Color(hex: "9970ff"))
|
||||||
|
.keyboardType(.default)
|
||||||
|
.padding(.top, 12)
|
||||||
|
.padding(.horizontal, 6.7)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.frame(height: 1)
|
||||||
|
.foregroundColor(Color(hex: "909090").opacity(0.7))
|
||||||
|
.padding(.top, 8.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func ContentInputView() -> some View {
|
||||||
|
VStack(spacing: 13.3) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("공지")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("\(notice.count)자")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "ff5c49")) +
|
||||||
|
Text(" / 1000자")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "777777"))
|
||||||
|
}
|
||||||
|
|
||||||
|
TextViewWrapper(
|
||||||
|
text: $notice,
|
||||||
|
placeholder: placeholder,
|
||||||
|
textColorHex: "eeeeee",
|
||||||
|
backgroundColorHex: "303030"
|
||||||
|
)
|
||||||
|
.frame(width: screenSize().width - 26.7, height: 133.3)
|
||||||
|
.cornerRadius(6.7)
|
||||||
|
.padding(.top, 13.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
//
|
||||||
|
// LiveRoomProfileDialog.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct LiveRoomProfileDialog: View {
|
||||||
|
|
||||||
|
@Binding var isShowing: Bool
|
||||||
|
let profileInfo: LiveRoomMember
|
||||||
|
let creatorId: Int
|
||||||
|
let isSpeaker: Bool
|
||||||
|
|
||||||
|
let memberId = UserDefaults.int(forKey: .userId)
|
||||||
|
|
||||||
|
var onClickInviteSpeaker: ((Int) -> Void)? = nil
|
||||||
|
var onClickChangeListener: ((Int) -> Void)? = nil
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Color.black.opacity(0.7).ignoresSafeArea()
|
||||||
|
.onTapGesture {
|
||||||
|
isShowing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
KFImage(URL(string: profileInfo.profileImage))
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 80, height: 116.7, alignment: .top)
|
||||||
|
.clipped()
|
||||||
|
.cornerRadius(13.3)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
Text(profileInfo.nickname)
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 20))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.padding(.top, 6.7)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if isSpeaker {
|
||||||
|
if profileInfo.role == .LISTENER, let onClickInviteSpeaker = onClickInviteSpeaker {
|
||||||
|
Text("스피커로 초대")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.padding(.horizontal, 15.4)
|
||||||
|
.padding(.vertical, 8.3)
|
||||||
|
.background(Color.white)
|
||||||
|
.cornerRadius(13.3)
|
||||||
|
.onTapGesture {
|
||||||
|
onClickInviteSpeaker(profileInfo.id)
|
||||||
|
isShowing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberId == creatorId || memberId == profileInfo.id) && profileInfo.id != creatorId && profileInfo.role == .SPEAKER,
|
||||||
|
let onClickChangeListener = onClickChangeListener {
|
||||||
|
Text("리스너로 변경")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.padding(.horizontal, 15.4)
|
||||||
|
.padding(.vertical, 8.3)
|
||||||
|
.background(Color.white)
|
||||||
|
.cornerRadius(13.3)
|
||||||
|
.onTapGesture {
|
||||||
|
onClickChangeListener(profileInfo.id)
|
||||||
|
isShowing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 116.7)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
.frame(width: screenSize().width - 53.4)
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.cornerRadius(16.7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
//
|
||||||
|
// LiveRoomProfileItemTitleView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct LiveRoomProfileItemTitleView: View {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
let count: Int?
|
||||||
|
let totalCount: Int?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text(title)
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 13))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
if let count = count {
|
||||||
|
Text("\(count)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.padding(.leading, 6.7)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let totalCount = totalCount {
|
||||||
|
Text("/\(totalCount > 9 ? 9 : totalCount - 1)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveRoomProfileItemMasterView: View {
|
||||||
|
|
||||||
|
let id: Int
|
||||||
|
let nickname: String
|
||||||
|
let profileUrl: String
|
||||||
|
let onClickProfile: (Int) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
KFImage(URL(string: profileUrl))
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.onTapGesture { onClickProfile(id) }
|
||||||
|
|
||||||
|
Image("ic_crown")
|
||||||
|
.padding(.leading, 16.7)
|
||||||
|
|
||||||
|
Text(nickname)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.padding(.leading, 4)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16.7)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.frame(height: 1)
|
||||||
|
.foregroundColor(Color(hex: "909090").opacity(0.3))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveRoomProfileItemUserView: View {
|
||||||
|
let isStaff: Bool
|
||||||
|
let userId: Int
|
||||||
|
let nickname: String
|
||||||
|
let profileUrl: String
|
||||||
|
let role: LiveRoomMemberRole
|
||||||
|
|
||||||
|
let onClickChangeListener: (Int) -> Void
|
||||||
|
let onClickInviteSpeaker: (Int) -> Void
|
||||||
|
let onClickKickOut: (Int) -> Void
|
||||||
|
let onClickProfile: (Int) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
KFImage(URL(string: profileUrl))
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 46.7, height: 46.7)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.onTapGesture { onClickProfile(userId) }
|
||||||
|
|
||||||
|
if role == .MANAGER {
|
||||||
|
Image("ic_badge_manager")
|
||||||
|
.padding(.leading, 16.7)
|
||||||
|
|
||||||
|
Text(nickname)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.lineLimit(2)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.padding(.leading, 4)
|
||||||
|
.padding(.trailing, 10)
|
||||||
|
} else {
|
||||||
|
Text(nickname)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.lineLimit(2)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if role == .LISTENER && isStaff {
|
||||||
|
Text("스피커로 초대")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10))
|
||||||
|
.foregroundColor(Color(hex: "ffffff"))
|
||||||
|
.padding(.horizontal, 5.5)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(Color(hex: "9970ff").opacity(0.3))
|
||||||
|
.cornerRadius(6.7)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 6.7)
|
||||||
|
.stroke(Color(hex: "9970ff"), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
onClickInviteSpeaker(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if role == .SPEAKER && (userId == UserDefaults.int(forKey: .userId) || isStaff) {
|
||||||
|
Text("리스너로 변경")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10))
|
||||||
|
.foregroundColor(Color(hex: "ffffff"))
|
||||||
|
.padding(.horizontal, 5.5)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.cornerRadius(6.7)
|
||||||
|
.onTapGesture {
|
||||||
|
onClickChangeListener(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if role != .MANAGER && isStaff {
|
||||||
|
Image("ic_kick_out")
|
||||||
|
.padding(.leading, 10)
|
||||||
|
.onTapGesture {
|
||||||
|
onClickKickOut(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.frame(height: 1)
|
||||||
|
.foregroundColor(Color(hex: "909090").opacity(0.3))
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16.7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveRoomProfileRequestSpeakerView: View {
|
||||||
|
|
||||||
|
let onClickRequestSpeaker: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 6.7) {
|
||||||
|
Spacer()
|
||||||
|
Image("ic_request_speak")
|
||||||
|
Text("스피커 요청하기")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 5.3)
|
||||||
|
.stroke(Color(hex: "909090"), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
onClickRequestSpeaker()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16.7)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,249 @@
|
||||||
|
//
|
||||||
|
// LiveRoomProfilesDialogView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct LiveRoomProfilesDialogView: View {
|
||||||
|
|
||||||
|
@Binding var isShowing: Bool
|
||||||
|
|
||||||
|
let viewModel: LiveRoomViewModel
|
||||||
|
let roomInfo: GetRoomInfoResponse
|
||||||
|
|
||||||
|
var profiles: [AnyView] = []
|
||||||
|
let accountId = UserDefaults.int(forKey: .userId)
|
||||||
|
|
||||||
|
init(
|
||||||
|
isShowing: Binding<Bool>,
|
||||||
|
viewModel: LiveRoomViewModel,
|
||||||
|
roomInfo: GetRoomInfoResponse,
|
||||||
|
isShowRequestSpeaker: Bool,
|
||||||
|
onClickRequestSpeaker: @escaping () -> Void,
|
||||||
|
registerNotification: @escaping () -> Void,
|
||||||
|
unRegisterNotification: @escaping () -> Void,
|
||||||
|
onClickProfile: @escaping (Int) -> Void
|
||||||
|
) {
|
||||||
|
self._isShowing = isShowing
|
||||||
|
self.viewModel = viewModel
|
||||||
|
self.roomInfo = roomInfo
|
||||||
|
|
||||||
|
self.profiles.append(
|
||||||
|
AnyView(
|
||||||
|
LiveRoomProfileItemTitleView(
|
||||||
|
title: "스탭",
|
||||||
|
count: roomInfo.managerList.count,
|
||||||
|
totalCount: nil
|
||||||
|
)
|
||||||
|
.padding(.vertical, 14)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
let isStaff = viewModel.isEqualToStaffId(creatorId: UserDefaults.int(forKey: .userId)) ||
|
||||||
|
roomInfo.creatorId == UserDefaults.int(forKey: .userId)
|
||||||
|
|
||||||
|
for manager in roomInfo.managerList {
|
||||||
|
self.profiles.append(
|
||||||
|
AnyView(
|
||||||
|
LiveRoomProfileItemUserView(
|
||||||
|
isStaff: isStaff ,
|
||||||
|
userId: manager.id,
|
||||||
|
nickname: manager.nickname,
|
||||||
|
profileUrl: manager.profileImage,
|
||||||
|
role: manager.role,
|
||||||
|
onClickChangeListener: { _ in },
|
||||||
|
onClickInviteSpeaker: { _ in },
|
||||||
|
onClickKickOut: { _ in },
|
||||||
|
onClickProfile: onClickProfile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.profiles.append(
|
||||||
|
AnyView(
|
||||||
|
LiveRoomProfileItemTitleView(
|
||||||
|
title: "스피커",
|
||||||
|
count: roomInfo.speakerList.count - 1,
|
||||||
|
totalCount: roomInfo.totalAvailableParticipantsCount
|
||||||
|
)
|
||||||
|
.padding(.vertical, 14)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for speaker in roomInfo.speakerList {
|
||||||
|
if speaker.id == roomInfo.creatorId {
|
||||||
|
self.profiles.insert(
|
||||||
|
AnyView(
|
||||||
|
LiveRoomProfileItemMasterView(
|
||||||
|
id: speaker.id,
|
||||||
|
nickname: speaker.nickname,
|
||||||
|
profileUrl: speaker.profileImage,
|
||||||
|
onClickProfile: onClickProfile
|
||||||
|
)
|
||||||
|
),
|
||||||
|
at: 0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self.profiles.append(
|
||||||
|
AnyView(
|
||||||
|
LiveRoomProfileItemUserView(
|
||||||
|
isStaff: isStaff,
|
||||||
|
userId: speaker.id,
|
||||||
|
nickname: speaker.nickname,
|
||||||
|
profileUrl: speaker.profileImage,
|
||||||
|
role: speaker.role,
|
||||||
|
onClickChangeListener: {
|
||||||
|
if $0 == UserDefaults.int(forKey: .userId) {
|
||||||
|
viewModel.setListener()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.changeListener(peerId: $0)
|
||||||
|
},
|
||||||
|
onClickInviteSpeaker: { _ in },
|
||||||
|
onClickKickOut: {
|
||||||
|
viewModel.kickOutId = $0
|
||||||
|
viewModel.isShowKickOutPopup = true
|
||||||
|
},
|
||||||
|
onClickProfile: onClickProfile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isShowRequestSpeaker {
|
||||||
|
self.profiles.append(
|
||||||
|
AnyView(
|
||||||
|
LiveRoomProfileRequestSpeakerView {
|
||||||
|
onClickRequestSpeaker()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.profiles.append(
|
||||||
|
AnyView(
|
||||||
|
LiveRoomProfileItemTitleView(
|
||||||
|
title: "리스너",
|
||||||
|
count: nil,
|
||||||
|
totalCount: nil
|
||||||
|
)
|
||||||
|
.padding(.top, 20)
|
||||||
|
.padding(.bottom, 14)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for listener in roomInfo.listenerList {
|
||||||
|
self.profiles.append(
|
||||||
|
AnyView(
|
||||||
|
LiveRoomProfileItemUserView(
|
||||||
|
isStaff: isStaff,
|
||||||
|
userId: listener.id,
|
||||||
|
nickname: listener.nickname,
|
||||||
|
profileUrl: listener.profileImage,
|
||||||
|
role: listener.role,
|
||||||
|
onClickChangeListener: { _ in },
|
||||||
|
onClickInviteSpeaker: {
|
||||||
|
if viewModel.liveRoomInfo!.speakerList.count <= 9 {
|
||||||
|
viewModel.inviteSpeaker(peerId: $0)
|
||||||
|
viewModel.popupContent = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요."
|
||||||
|
viewModel.isShowPopup = true
|
||||||
|
} else {
|
||||||
|
viewModel.errorMessage = "스피커 정원을 초과했습니다."
|
||||||
|
viewModel.isShowErrorPopup = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClickKickOut: {
|
||||||
|
viewModel.kickOutId = $0
|
||||||
|
viewModel.isShowKickOutPopup = true
|
||||||
|
},
|
||||||
|
onClickProfile: onClickProfile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
VStack(spacing: 16.7) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("참여자")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 15))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Text("\(roomInfo.participantsCount)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.padding(.leading, 6.7)
|
||||||
|
|
||||||
|
Text("/\(roomInfo.totalAvailableParticipantsCount)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("ic_close_white")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
.onTapGesture { isShowing = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
ForEach(0..<profiles.count, id: \.self) { index in
|
||||||
|
profiles[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 26.7)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.background(Color(hex: "222222").edgesIgnoringSafeArea(.all))
|
||||||
|
.cornerRadius(16.7)
|
||||||
|
|
||||||
|
if viewModel.isShowPopup {
|
||||||
|
LiveRoomDialogView(
|
||||||
|
content: viewModel.popupContent,
|
||||||
|
cancelTitle: viewModel.popupCancelTitle,
|
||||||
|
cancelAction: viewModel.popupCancelAction,
|
||||||
|
confirmTitle: viewModel.popupConfirmTitle,
|
||||||
|
confirmAction: viewModel.popupConfirmAction
|
||||||
|
).onAppear {
|
||||||
|
if viewModel.popupConfirmTitle == nil && viewModel.popupConfirmAction == nil {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||||
|
viewModel.isShowPopup = false
|
||||||
|
viewModel.popupCancelTitle = nil
|
||||||
|
viewModel.popupCancelAction = nil
|
||||||
|
viewModel.popupConfirmTitle = nil
|
||||||
|
viewModel.popupConfirmAction = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isShowKickOutPopup {
|
||||||
|
SodaDialog(
|
||||||
|
title: "내보내기",
|
||||||
|
desc: viewModel.kickOutDesc,
|
||||||
|
confirmButtonTitle: "내보내기",
|
||||||
|
confirmButtonAction: {
|
||||||
|
viewModel.kickOut()
|
||||||
|
},
|
||||||
|
cancelButtonTitle: "취소",
|
||||||
|
cancelButtonAction: {
|
||||||
|
viewModel.isShowKickOutPopup = false
|
||||||
|
viewModel.kickOutDesc = ""
|
||||||
|
viewModel.kickOutId = 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,259 @@
|
||||||
|
//
|
||||||
|
// LiveRoomUserProfileDialogView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct LiveRoomUserProfileDialogView: View {
|
||||||
|
|
||||||
|
@Binding var isShowing: Bool
|
||||||
|
|
||||||
|
@State private var introduceLineLimit: Int? = 2
|
||||||
|
|
||||||
|
let viewModel: LiveRoomViewModel
|
||||||
|
let userProfile: GetLiveRoomUserProfileResponse
|
||||||
|
let onClickSetManager: (Int) -> Void
|
||||||
|
let onClickReleaseManager: (Int) -> Void
|
||||||
|
let onClickFollow: (Int) -> Void
|
||||||
|
let onClickUnFollow: (Int) -> Void
|
||||||
|
let onClickInviteSpeaker: (Int) -> Void
|
||||||
|
let onClickChangeListener: (Int) -> Void
|
||||||
|
let onClickMenu: (Int, String, Bool) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("프로필")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 15))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("ic_close_white")
|
||||||
|
.onTapGesture {
|
||||||
|
isShowing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 13.3)
|
||||||
|
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Text(userProfile.nickname)
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Text(userProfile.gender)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 11.3))
|
||||||
|
.foregroundColor(Color(hex: "ffffff"))
|
||||||
|
.padding(.horizontal, 5.3)
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.background(Color(hex: "555555"))
|
||||||
|
.cornerRadius(23.3)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("ic_seemore_vertical")
|
||||||
|
.onTapGesture {
|
||||||
|
onClickMenu(
|
||||||
|
userProfile.userId,
|
||||||
|
userProfile.nickname,
|
||||||
|
userProfile.isBlock
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 21.3)
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
if let isFollwing = userProfile.isFollowing {
|
||||||
|
if isFollwing {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image("ic_alarm_selected")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 18.7, height: 18.7)
|
||||||
|
|
||||||
|
Text("팔로잉")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "ffffff"))
|
||||||
|
}
|
||||||
|
.padding(.vertical, 7.3)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(Color(hex: "3e1b93"))
|
||||||
|
.cornerRadius(16.7)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 16.7)
|
||||||
|
.strokeBorder(lineWidth: 1)
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
)
|
||||||
|
.onTapGesture { onClickUnFollow(userProfile.userId) }
|
||||||
|
} else {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image("ic_alarm")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 18.7, height: 18.7)
|
||||||
|
|
||||||
|
Text("팔로우")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "ffffff"))
|
||||||
|
}
|
||||||
|
.padding(.vertical, 7.3)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.cornerRadius(16.7)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 16.7)
|
||||||
|
.strokeBorder(lineWidth: 1)
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
)
|
||||||
|
.onTapGesture { onClickFollow(userProfile.userId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image("ic_message_send")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 18.7, height: 18.7)
|
||||||
|
|
||||||
|
Text("메시지 보내기")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "ffffff"))
|
||||||
|
}
|
||||||
|
.padding(.vertical, 7.3)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.cornerRadius(16.7)
|
||||||
|
.onTapGesture {
|
||||||
|
AppState.shared.setAppStep(step: .writeTextMessage(
|
||||||
|
userId: userProfile.userId,
|
||||||
|
nickname: userProfile.nickname))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.padding(.top, 21.3)
|
||||||
|
|
||||||
|
KFImage(URL(string: userProfile.profileUrl))
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(CGSize(width: 1, height: 1), contentMode: .fill)
|
||||||
|
.cornerRadius(8)
|
||||||
|
.padding(.top, 21.3)
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
if let isSpeaker = userProfile.isSpeaker {
|
||||||
|
Text(isSpeaker ? "리스너 변경" : "스피커 초대")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 15))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 13)
|
||||||
|
.cornerRadius(8)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.strokeBorder(lineWidth: 1)
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
if isSpeaker {
|
||||||
|
onClickChangeListener(userProfile.userId)
|
||||||
|
} else {
|
||||||
|
onClickInviteSpeaker(userProfile.userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
isShowing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let isManager = userProfile.isManager {
|
||||||
|
Text(isManager ? "스탭 해제" : "스탭 지정")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 15))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 13)
|
||||||
|
.cornerRadius(8)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.strokeBorder(lineWidth: 1)
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
if isManager {
|
||||||
|
onClickReleaseManager(userProfile.userId)
|
||||||
|
} else {
|
||||||
|
onClickSetManager(userProfile.userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
isShowing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userProfile.isSpeaker != nil && !viewModel.isEqualToStaffId(creatorId: userProfile.userId)) ||
|
||||||
|
(userProfile.isSpeaker != nil && userProfile.isManager != nil) {
|
||||||
|
Text("내보내기")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 15))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 13)
|
||||||
|
.cornerRadius(8)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.strokeBorder(lineWidth: 1)
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.kickOutId = userProfile.userId
|
||||||
|
viewModel.isShowKickOutPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.padding(.top, 21.3)
|
||||||
|
|
||||||
|
Text(userProfile.tags)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.lineSpacing(3)
|
||||||
|
.padding(.top, 21.3)
|
||||||
|
|
||||||
|
Text(userProfile.introduce)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "909090"))
|
||||||
|
.lineLimit(introduceLineLimit)
|
||||||
|
.lineSpacing(3)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.padding(.vertical, 20)
|
||||||
|
.onTapGesture {
|
||||||
|
if let _ = introduceLineLimit {
|
||||||
|
self.introduceLineLimit = nil
|
||||||
|
} else {
|
||||||
|
self.introduceLineLimit = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.background(Color(hex: "222222").edgesIgnoringSafeArea(.all))
|
||||||
|
.cornerRadius(8)
|
||||||
|
|
||||||
|
if viewModel.isShowKickOutPopup {
|
||||||
|
SodaDialog(
|
||||||
|
title: "내보내기",
|
||||||
|
desc: viewModel.kickOutDesc,
|
||||||
|
confirmButtonTitle: "내보내기",
|
||||||
|
confirmButtonAction: {
|
||||||
|
viewModel.kickOut()
|
||||||
|
isShowing = false
|
||||||
|
},
|
||||||
|
cancelButtonTitle: "취소",
|
||||||
|
cancelButtonAction: {
|
||||||
|
viewModel.isShowKickOutPopup = false
|
||||||
|
viewModel.kickOutDesc = ""
|
||||||
|
viewModel.kickOutId = 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
//
|
||||||
|
// GetLiveRoomDonationStatusResponse.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct GetLiveRoomDonationStatusResponse: Decodable {
|
||||||
|
let donationList: [GetLiveRoomDonationItem]
|
||||||
|
let totalCount: Int
|
||||||
|
let totalCan: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GetLiveRoomDonationItem: Decodable {
|
||||||
|
let profileImage: String
|
||||||
|
let nickname: String
|
||||||
|
let userId: Int
|
||||||
|
let can: Int
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
//
|
||||||
|
// GetLiveRoomDonationTotalResponse.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct GetLiveRoomDonationTotalResponse: Decodable {
|
||||||
|
let totalDonationCan: Int
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
// GetLiveRoomUserProfileResponse.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct GetLiveRoomUserProfileResponse: Decodable {
|
||||||
|
let userId: Int
|
||||||
|
let nickname: String
|
||||||
|
let profileUrl: String
|
||||||
|
let gender: String
|
||||||
|
let instagramUrl: String
|
||||||
|
let youtubeUrl: String
|
||||||
|
let websiteUrl: String
|
||||||
|
let blogUrl: String
|
||||||
|
let introduce: String
|
||||||
|
let tags: String
|
||||||
|
let isSpeaker: Bool?
|
||||||
|
let isManager: Bool?
|
||||||
|
let isFollowing: Bool?
|
||||||
|
let isBlock: Bool
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
//
|
||||||
|
// GetMemberCanResponse.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct GetMemberCanResponse: Decodable {
|
||||||
|
let can: Int
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
//
|
||||||
|
// GetRoomInfoResponse.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
struct GetRoomInfoResponse: Decodable {
|
||||||
|
let roomId: Int
|
||||||
|
let title: String
|
||||||
|
let notice: String
|
||||||
|
let coverImageUrl: String
|
||||||
|
let channelName: String
|
||||||
|
let rtcToken: String
|
||||||
|
let rtmToken: String
|
||||||
|
let creatorId: Int
|
||||||
|
let creatorNickname: String
|
||||||
|
let creatorProfileUrl: String
|
||||||
|
let isFollowing: Bool
|
||||||
|
let participantsCount: Int
|
||||||
|
let totalAvailableParticipantsCount: Int
|
||||||
|
let speakerList: [LiveRoomMember]
|
||||||
|
let listenerList: [LiveRoomMember]
|
||||||
|
let managerList: [LiveRoomMember]
|
||||||
|
let donationRankingTop3UserIds: [Int]
|
||||||
|
let isPrivateRoom: Bool
|
||||||
|
let password: String?
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
//
|
||||||
|
// LiveRoomDonationMessage.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct LiveRoomDonationMessage: Decodable {
|
||||||
|
let uuid: String
|
||||||
|
let nickname: String
|
||||||
|
let canMessage: String
|
||||||
|
let donationMessage: String
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
//
|
||||||
|
// LiveRoomDonationRequest.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct LiveRoomDonationRequest: Encodable {
|
||||||
|
let roomId: Int
|
||||||
|
let can: Int
|
||||||
|
let message: String
|
||||||
|
let container: String = "ios"
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// LiveRoomKickOutRequest.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct LiveRoomKickOutRequest: Encodable {
|
||||||
|
let roomId: Int
|
||||||
|
let userId: Int
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
//
|
||||||
|
// LiveRoomMember.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
struct LiveRoomMember: Decodable, Hashable {
|
||||||
|
let id: Int
|
||||||
|
let nickname: String
|
||||||
|
let profileImage: String
|
||||||
|
let role: LiveRoomMemberRole
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LiveRoomMemberRole: String, Decodable {
|
||||||
|
case LISTENER, SPEAKER, MANAGER
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LiveRoomProfileItemType: String {
|
||||||
|
case MASTER, SPEAKER_TITLE, LISTENER_TITLE, USER
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol LiveRoomProfileItem: Hashable {
|
||||||
|
var type: LiveRoomProfileItemType { get set }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveRoomProfileItemSpeakerTitle: LiveRoomProfileItem {
|
||||||
|
var type: LiveRoomProfileItemType = .SPEAKER_TITLE
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveRoomProfileItemListenerTitle: LiveRoomProfileItem {
|
||||||
|
var type: LiveRoomProfileItemType = .LISTENER_TITLE
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveRoomProfileItemMaster: LiveRoomProfileItem {
|
||||||
|
var type: LiveRoomProfileItemType = .MASTER
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveRoomProfileItemUser: LiveRoomProfileItem {
|
||||||
|
var type: LiveRoomProfileItemType = .USER
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
//
|
||||||
|
// LiveRoomRequestType.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum LiveRoomRequestType: String {
|
||||||
|
case REQUEST_SPEAKER, REQUEST_SPEAKER_ALLOW, INVITE_SPEAKER, CHANGE_LISTENER, KICK_OUT, SET_MANAGER, RELEASE_MANAGER
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
//
|
||||||
|
// LiveRoomTopCreatorView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct LiveRoomTopCreatorView: View {
|
||||||
|
|
||||||
|
var nickname: String
|
||||||
|
var profileImageUrl: String
|
||||||
|
var isFollowing: Bool
|
||||||
|
var onClickProfile: () -> Void
|
||||||
|
var onClickFollow: (Bool) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 5.3) {
|
||||||
|
KFImage(URL(string: profileImageUrl))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 33.3, height: 33.3)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.onTapGesture { onClickProfile() }
|
||||||
|
|
||||||
|
Image("ic_crown")
|
||||||
|
|
||||||
|
Text(nickname)
|
||||||
|
.font(.custom(Font.light.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(isFollowing ? "btn_following" : "btn_follow")
|
||||||
|
.onTapGesture { onClickFollow(isFollowing) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,932 @@
|
||||||
|
//
|
||||||
|
// LiveRoomView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
import PopupView
|
||||||
|
|
||||||
|
struct LiveRoomView: View {
|
||||||
|
@State private var isShowingNewChat = false
|
||||||
|
@State private var isShowPhotoPicker = false
|
||||||
|
|
||||||
|
let columns = [
|
||||||
|
GridItem(.flexible()),
|
||||||
|
GridItem(.flexible()),
|
||||||
|
GridItem(.flexible()),
|
||||||
|
GridItem(.flexible()),
|
||||||
|
GridItem(.flexible())
|
||||||
|
]
|
||||||
|
|
||||||
|
let chatColumns = [GridItem(.flexible())]
|
||||||
|
|
||||||
|
@StateObject var keyboardHandler = KeyboardHandler()
|
||||||
|
@StateObject var viewModel = LiveRoomViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Color.black.edgesIgnoringSafeArea(.all)
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
HStack(spacing: 6.7) {
|
||||||
|
Text(
|
||||||
|
UserDefaults.int(forKey: .userId) == viewModel.liveRoomInfo?.creatorId ?
|
||||||
|
"라이브 종료":
|
||||||
|
"나가기"
|
||||||
|
)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10))
|
||||||
|
.foregroundColor(Color(hex: "ff5c49"))
|
||||||
|
.padding(.horizontal, 14.3)
|
||||||
|
.padding(.vertical, 8.3)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 13.3)
|
||||||
|
.stroke(Color(hex: "ff5c49"), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
if let liveRoomInfo = viewModel.liveRoomInfo, liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) {
|
||||||
|
viewModel.isShowLiveEndPopup = true
|
||||||
|
} else {
|
||||||
|
viewModel.isShowQuitPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text(viewModel.isBgOn ? "배경 ON" : "배경 OFF")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10))
|
||||||
|
.foregroundColor(Color(hex: viewModel.isBgOn ? "9970ff" : "eeeeee"))
|
||||||
|
.padding(.horizontal, 14.3)
|
||||||
|
.padding(.vertical, 8.3)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 13.3)
|
||||||
|
.stroke(Color(hex: viewModel.isBgOn ? "9970ff" : "bbbbbb"), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.isBgOn.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 4.7) {
|
||||||
|
Image("ic_share")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 16, height: 16)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 14.3)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 13.3)
|
||||||
|
.stroke(Color(hex: "bbbbbb"), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.shareRoom()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let liveRoomInfo = viewModel.liveRoomInfo,
|
||||||
|
liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) {
|
||||||
|
HStack(spacing: 4.7) {
|
||||||
|
Image("ic_edit")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 16, height: 16)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 14.3)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 13.3)
|
||||||
|
.stroke(Color(hex: "bbbbbb"), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.isShowEditRoomInfoDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
|
||||||
|
if let liveRoomInfo = viewModel.liveRoomInfo {
|
||||||
|
ZStack {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
Text(liveRoomInfo.title)
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 15.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.top, 16.7)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
|
||||||
|
LiveRoomTopCreatorView(
|
||||||
|
nickname: liveRoomInfo.creatorNickname,
|
||||||
|
profileImageUrl: liveRoomInfo.creatorProfileUrl,
|
||||||
|
isFollowing: liveRoomInfo.isFollowing,
|
||||||
|
onClickProfile: {
|
||||||
|
if liveRoomInfo.creatorId != UserDefaults.int(forKey: .userId) {
|
||||||
|
viewModel.getUserProfile(userId: liveRoomInfo.creatorId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClickFollow: {
|
||||||
|
if $0 {
|
||||||
|
viewModel.unRegisterNotification()
|
||||||
|
} else {
|
||||||
|
viewModel.registerNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(.top, 16.7)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.frame(height: 1)
|
||||||
|
.foregroundColor(Color(hex: "909090").opacity(0.3))
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.padding(.top, 8)
|
||||||
|
|
||||||
|
NotificationView(liveRoomInfo: liveRoomInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isMute {
|
||||||
|
Image("img_noti_mute")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isShowNotice {
|
||||||
|
HStack(alignment: .top, spacing: 8) {
|
||||||
|
Text("[공지]")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 11.3))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
AttributedTextView(
|
||||||
|
attributedString: makeAttributedString(liveRoomInfo.notice),
|
||||||
|
lineLimit: viewModel.isExpandNotice ? Int.max : 1
|
||||||
|
) {
|
||||||
|
UIApplication.shared.open($0)
|
||||||
|
|
||||||
|
}
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.lineSpacing(6)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 26.7)
|
||||||
|
.padding(.vertical, 13.3)
|
||||||
|
.frame(width: screenSize().width, alignment: .leading)
|
||||||
|
.background(Color(hex: "3d2a6c"))
|
||||||
|
.padding(.top, 10)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.isExpandNotice.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !viewModel.isSpeakerFold {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("스피커")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.top, 20)
|
||||||
|
.padding(.horizontal, 23.3)
|
||||||
|
|
||||||
|
LazyVGrid(columns: columns) {
|
||||||
|
ForEach(liveRoomInfo.speakerList, id: \.self) { speaker in
|
||||||
|
VStack(spacing: 6.7) {
|
||||||
|
ZStack {
|
||||||
|
KFImage(URL(string: speaker.profileImage))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 46.7, height: 46.7, alignment: .top)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.overlay(
|
||||||
|
Circle()
|
||||||
|
.stroke(
|
||||||
|
Color(hex: "9970ff"),
|
||||||
|
lineWidth: viewModel.activeSpeakers.contains(UInt(speaker.id)) ? 3 : 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if viewModel.muteSpeakers.contains(UInt(speaker.id)) {
|
||||||
|
Image("ic_mute")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 46.7, height: 46.7)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if liveRoomInfo.creatorId == speaker.id {
|
||||||
|
Image("ic_crown")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 16.7, height: 16.7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 46.7, height: 46.7)
|
||||||
|
|
||||||
|
Text(speaker.nickname)
|
||||||
|
.font(.custom(Font.light.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.selectedProfile = speaker
|
||||||
|
viewModel.isShowProfilePopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 16.7)
|
||||||
|
.padding(.horizontal, 23.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 16.7)
|
||||||
|
.frame(width: screenSize().width)
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
.cornerRadius(16.7, corners: [.topLeft, .topRight])
|
||||||
|
|
||||||
|
ZStack(alignment: .top) {
|
||||||
|
ScrollViewReader { proxy in
|
||||||
|
ZStack(alignment: .bottomTrailing) {
|
||||||
|
if let liveRoomInfo = viewModel.liveRoomInfo, viewModel.isBgOn {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
KFImage(URL(string: liveRoomInfo.coverImageUrl))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: proxy.size.width, height: proxy.size.height, alignment: .center)
|
||||||
|
.clipped()
|
||||||
|
|
||||||
|
Color.black.opacity(0.4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
scrollObservableView
|
||||||
|
ChatView()
|
||||||
|
.frame(width: screenSize().width)
|
||||||
|
}
|
||||||
|
.rotationEffect(Angle(degrees: 180))
|
||||||
|
.onTapGesture { hideKeyboard() }
|
||||||
|
.onPreferenceChange(ScrollOffsetKey.self) {
|
||||||
|
viewModel.setOffset($0)
|
||||||
|
}
|
||||||
|
|
||||||
|
InputChatView {
|
||||||
|
isShowingNewChat = false
|
||||||
|
proxy.scrollTo(viewModel.messages.count - 1, anchor: .center)
|
||||||
|
}.padding(.bottom, keyboardHandler.keyboardHeight > 0 ? 0 : 15)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 13.3) {
|
||||||
|
if viewModel.role == .SPEAKER {
|
||||||
|
Image(viewModel.isMute ? "ic_mic_off" : "ic_mic_on")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 26.7, height: 26.7)
|
||||||
|
.padding(11)
|
||||||
|
.background(Color(hex: "525252").opacity(0.6))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.padding(.bottom, 13.3)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.toggleMute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(viewModel.isSpeakerMute ? "ic_speaker_off" : "ic_speaker_on")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 26.7, height: 26.7)
|
||||||
|
.padding(11)
|
||||||
|
.background(Color(hex: "525252").opacity(0.6))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.padding(.bottom, 13.3)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.toggleSpeakerMute()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let liveRoomInfo = viewModel.liveRoomInfo, liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) && UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue {
|
||||||
|
Image("ic_donation_message_list")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 26.7, height: 26.7)
|
||||||
|
.padding(11)
|
||||||
|
.background(Color(hex: "525252").opacity(0.6))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.isShowDonationMessagePopup = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Image("ic_donation")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 26.7, height: 26.7)
|
||||||
|
.padding(11)
|
||||||
|
.background(Color(hex: "525252").opacity(0.6))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.isShowDonationPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.trailing, 16.7)
|
||||||
|
.padding(.bottom, 85)
|
||||||
|
|
||||||
|
if isShowingNewChat {
|
||||||
|
NewChatView{
|
||||||
|
isShowingNewChat = false
|
||||||
|
proxy.scrollTo(viewModel.messages.count - 1, anchor: .center)
|
||||||
|
}.padding(.bottom, 70)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: screenSize().width)
|
||||||
|
.animation(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(spacing: 6.7) {
|
||||||
|
Image(viewModel.isSpeakerFold ? "ic_live_detail_bottom" : "ic_live_detail_top")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
|
||||||
|
Text(viewModel.isSpeakerFold ? "펼치기" : "접기")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
}
|
||||||
|
.padding(.vertical, 6.7)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.isSpeakerFold.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(
|
||||||
|
LinearGradient(
|
||||||
|
gradient: Gradient(colors: [Color(hex: "222222").opacity(0.95), Color.black.opacity(0.005)]),
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
).ignoresSafeArea()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.popup(isPresented: $viewModel.isShowErrorPopup, type: .toast, position: .top, autohideIn: 1.3) {
|
||||||
|
GeometryReader { geo in
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text(viewModel.errorMessage)
|
||||||
|
.padding(.vertical, 13.3)
|
||||||
|
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.cornerRadius(20)
|
||||||
|
.padding(.top, 66.7)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
viewModel.quitRoom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cornerRadius(16.7, corners: [.topLeft, .topRight])
|
||||||
|
.offset(y: -(keyboardHandler.keyboardHeight > 0 ? keyboardHandler.keyboardHeight : 0))
|
||||||
|
.onAppear {
|
||||||
|
UIApplication.shared.isIdleTimerDisabled = true
|
||||||
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
|
|
||||||
|
viewModel.getMemberCan()
|
||||||
|
viewModel.initAgoraEngine()
|
||||||
|
viewModel.getRoomInfo()
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
forName: UIApplication.willTerminateNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main) { _ in
|
||||||
|
viewModel.quitRoom()
|
||||||
|
sleep(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
UIApplication.shared.isIdleTimerDisabled = false
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
ZStack {
|
||||||
|
if viewModel.isShowProfilePopup, let liveRoomInfo = viewModel.liveRoomInfo, let selectedProfile = viewModel.selectedProfile {
|
||||||
|
LiveRoomProfileDialog(
|
||||||
|
isShowing: $viewModel.isShowProfilePopup,
|
||||||
|
profileInfo: selectedProfile,
|
||||||
|
creatorId: liveRoomInfo.creatorId,
|
||||||
|
isSpeaker: viewModel.role == .SPEAKER,
|
||||||
|
onClickInviteSpeaker: { inviteSpeaker(peerId: $0) },
|
||||||
|
onClickChangeListener: {
|
||||||
|
if $0 == UserDefaults.int(forKey: .userId) {
|
||||||
|
viewModel.setListener()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.changeListener(peerId: $0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isShowDonationPopup {
|
||||||
|
LiveRoomDonationDialogView(isShowing: $viewModel.isShowDonationPopup, isAudioContentDonation: false) { can, message in
|
||||||
|
viewModel.donation(can: can, message: message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isShowQuitPopup {
|
||||||
|
SodaDialog(
|
||||||
|
title: "라이브 나가기",
|
||||||
|
desc: "라이브에서 나가시겠습니까?",
|
||||||
|
confirmButtonTitle: "예",
|
||||||
|
confirmButtonAction: {
|
||||||
|
viewModel.isShowQuitPopup = false
|
||||||
|
viewModel.quitRoom()
|
||||||
|
},
|
||||||
|
cancelButtonTitle: "아니오",
|
||||||
|
cancelButtonAction: {
|
||||||
|
viewModel.isShowQuitPopup = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isShowLiveEndPopup {
|
||||||
|
SodaDialog(
|
||||||
|
title: "라이브 종료",
|
||||||
|
desc: "라이브를 종료하시겠습니까?\n" +
|
||||||
|
"라이브를 종료하면 대화내용은\n" +
|
||||||
|
"저장되지 않고 사라집니다.\n" +
|
||||||
|
"참여자들 또한 라이브가 종료되어\n" +
|
||||||
|
"강제퇴장 됩니다.",
|
||||||
|
confirmButtonTitle: "예",
|
||||||
|
confirmButtonAction: {
|
||||||
|
viewModel.isShowLiveEndPopup = false
|
||||||
|
viewModel.quitRoom()
|
||||||
|
},
|
||||||
|
cancelButtonTitle: "아니오",
|
||||||
|
cancelButtonAction: {
|
||||||
|
viewModel.isShowLiveEndPopup = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isShowPopup {
|
||||||
|
LiveRoomDialogView(
|
||||||
|
content: viewModel.popupContent,
|
||||||
|
cancelTitle: viewModel.popupCancelTitle,
|
||||||
|
cancelAction: viewModel.popupCancelAction,
|
||||||
|
confirmTitle: viewModel.popupConfirmTitle,
|
||||||
|
confirmAction: viewModel.popupConfirmAction
|
||||||
|
).onAppear {
|
||||||
|
if viewModel.popupConfirmTitle == nil && viewModel.popupConfirmAction == nil {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||||
|
viewModel.isShowPopup = false
|
||||||
|
viewModel.popupCancelTitle = nil
|
||||||
|
viewModel.popupCancelAction = nil
|
||||||
|
viewModel.popupConfirmTitle = nil
|
||||||
|
viewModel.popupConfirmAction = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ZStack {
|
||||||
|
if viewModel.isShowProfileList, let liveRoomInfo = viewModel.liveRoomInfo {
|
||||||
|
LiveRoomProfilesDialogView(
|
||||||
|
isShowing: $viewModel.isShowProfileList,
|
||||||
|
viewModel: viewModel,
|
||||||
|
roomInfo: liveRoomInfo,
|
||||||
|
isShowRequestSpeaker: viewModel.role != .SPEAKER,
|
||||||
|
onClickRequestSpeaker: {
|
||||||
|
viewModel.requestSpeaker()
|
||||||
|
},
|
||||||
|
registerNotification: { viewModel.registerNotification() },
|
||||||
|
unRegisterNotification: { viewModel.unRegisterNotification() },
|
||||||
|
onClickProfile: {
|
||||||
|
if $0 != UserDefaults.int(forKey: .userId) {
|
||||||
|
viewModel.getUserProfile(userId: $0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isShowUserProfilePopup, let userProfile = viewModel.userProfile {
|
||||||
|
Color.black.opacity(0.7)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
|
||||||
|
LiveRoomUserProfileDialogView(
|
||||||
|
isShowing: $viewModel.isShowUserProfilePopup,
|
||||||
|
viewModel: viewModel,
|
||||||
|
userProfile: userProfile,
|
||||||
|
onClickSetManager: {
|
||||||
|
viewModel.setManagerMessageToPeer(userId: $0)
|
||||||
|
viewModel.setManager(userId: $0)
|
||||||
|
},
|
||||||
|
onClickReleaseManager: { viewModel.changeListener(peerId: $0, isFromManager: true) },
|
||||||
|
onClickFollow: { viewModel.registerNotification(creatorId: $0, isGetUserProfile: true) },
|
||||||
|
onClickUnFollow: { viewModel.unRegisterNotification(creatorId: $0, isGetUserProfile: true) },
|
||||||
|
onClickInviteSpeaker: { inviteSpeaker(peerId: $0) },
|
||||||
|
onClickChangeListener: {
|
||||||
|
viewModel.changeListener(peerId: $0)
|
||||||
|
},
|
||||||
|
onClickMenu: { userId, userNickname, isBlocked in
|
||||||
|
viewModel.reportUserId = userId
|
||||||
|
viewModel.reportUserNickname = userNickname
|
||||||
|
viewModel.reportUserIsBlocked = isBlocked
|
||||||
|
viewModel.isShowReportMenu = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(20)
|
||||||
|
.popup(isPresented: $viewModel.isShowReportPopup, type: .toast, position: .top, autohideIn: 1.3) {
|
||||||
|
GeometryReader { geo in
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text(viewModel.reportMessage)
|
||||||
|
.padding(.vertical, 13.3)
|
||||||
|
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.cornerRadius(20)
|
||||||
|
.padding(.top, 66.7)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isShowReportMenu {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
ProfileReportMenuView(
|
||||||
|
isShowing: $viewModel.isShowReportMenu,
|
||||||
|
isBlockedUser: viewModel.reportUserIsBlocked,
|
||||||
|
userBlockAction: { viewModel.isShowUesrBlockConfirm = true },
|
||||||
|
userUnBlockAction: { viewModel.userUnBlock() },
|
||||||
|
userReportAction: { viewModel.isShowUesrReportView = true },
|
||||||
|
profileReportAction: { viewModel.isShowProfileReportConfirm = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color(hex: "222222"))
|
||||||
|
.frame(width: screenSize().width, height: 15.3)
|
||||||
|
}
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isShowUesrBlockConfirm {
|
||||||
|
UserBlockConfirmDialogView(
|
||||||
|
isShowing: $viewModel.isShowUesrBlockConfirm,
|
||||||
|
nickname: viewModel.reportUserNickname,
|
||||||
|
confirmAction: { viewModel.userBlock() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 && viewModel.liveRoomInfo == nil {
|
||||||
|
LoadingView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea(.keyboard)
|
||||||
|
.edgesIgnoringSafeArea(keyboardHandler.keyboardHeight > 0 ? .bottom : .init())
|
||||||
|
.sheet(
|
||||||
|
isPresented: $viewModel.isShowShareView,
|
||||||
|
onDismiss: { viewModel.shareMessage = "" },
|
||||||
|
content: {
|
||||||
|
ActivityViewController(activityItems: [viewModel.shareMessage])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.sheet(isPresented: $isShowPhotoPicker) {
|
||||||
|
ImagePicker(
|
||||||
|
isShowing: $isShowPhotoPicker,
|
||||||
|
selectedImage: $viewModel.coverImage,
|
||||||
|
sourceType: .photoLibrary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $viewModel.isShowEditRoomInfoDialog) {
|
||||||
|
if let liveRoomInfo = viewModel.liveRoomInfo {
|
||||||
|
LiveRoomInfoEditDialog(
|
||||||
|
isShowing: $viewModel.isShowEditRoomInfoDialog,
|
||||||
|
isShowPhotoPicker: $isShowPhotoPicker,
|
||||||
|
viewModel: viewModel,
|
||||||
|
isLoading: viewModel.isLoading,
|
||||||
|
currentTitle: liveRoomInfo.title,
|
||||||
|
currentNotice: liveRoomInfo.notice,
|
||||||
|
coverImageUrl: liveRoomInfo.coverImageUrl,
|
||||||
|
coverImage: viewModel.coverImage
|
||||||
|
) { newTitle, newNotice in
|
||||||
|
self.viewModel.editLiveRoomInfo(
|
||||||
|
title: newTitle,
|
||||||
|
notice: newNotice
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
.onAppear {
|
||||||
|
viewModel.isShowEditRoomInfoDialog = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $viewModel.isShowDonationRankingPopup) {
|
||||||
|
LiveRoomDonationRankingDialog(isShowing: $viewModel.isShowDonationRankingPopup)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $viewModel.isShowDonationMessagePopup) {
|
||||||
|
LiveRoomDonationMessageDialog(isShowing: $viewModel.isShowDonationMessagePopup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAttributedString(_ text: String) -> NSAttributedString {
|
||||||
|
let attributedString = NSMutableAttributedString(string: text)
|
||||||
|
|
||||||
|
let urlRegex = try! NSRegularExpression(pattern: "\\b(https?://\\S+\\b|www\\.\\S+\\b)")
|
||||||
|
let matches = urlRegex.matches(in: text, options: [], range: NSRange(text.startIndex..., in: text))
|
||||||
|
|
||||||
|
for match in matches {
|
||||||
|
let url = (text as NSString).substring(with: match.range)
|
||||||
|
if let detectedURL = URL(string: url) {
|
||||||
|
attributedString.addAttribute(.link, value: detectedURL, range: match.range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributedString
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func inviteSpeaker(peerId: Int) {
|
||||||
|
if viewModel.liveRoomInfo!.speakerList.count <= 9 {
|
||||||
|
viewModel.inviteSpeaker(peerId: peerId)
|
||||||
|
self.viewModel.popupContent = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요."
|
||||||
|
self.viewModel.isShowPopup = true
|
||||||
|
} else {
|
||||||
|
viewModel.popupContent = "스피커 정원을 초과했습니다."
|
||||||
|
viewModel.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var scrollObservableView: some View {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
let offsetY = proxy.frame(in: .global).origin.y
|
||||||
|
Color.clear
|
||||||
|
.preference(
|
||||||
|
key: ScrollOffsetKey.self,
|
||||||
|
value: offsetY
|
||||||
|
)
|
||||||
|
.onAppear {
|
||||||
|
viewModel.setOriginOffset(offsetY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScrollOffsetKey: PreferenceKey {
|
||||||
|
static var defaultValue: CGFloat = .zero
|
||||||
|
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||||
|
value += nextValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func NotificationView(liveRoomInfo: GetRoomInfoResponse) -> some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(
|
||||||
|
viewModel.isShowNotice ?
|
||||||
|
"ic_notice_selected" :
|
||||||
|
"ic_notice_normal"
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.isShowNotice.toggle()
|
||||||
|
}
|
||||||
|
.padding(.trailing, 10)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(spacing: 4.7) {
|
||||||
|
Image("ic_donation_status")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 16, height: 16)
|
||||||
|
|
||||||
|
Text("\(viewModel.totalDonationCan)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 11.5)
|
||||||
|
.padding(.vertical, 5.3)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 12.8)
|
||||||
|
.strokeBorder(lineWidth: 1)
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.isShowDonationRankingPopup = true
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("참여자")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
|
||||||
|
Text("\(liveRoomInfo.participantsCount)")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.padding(.leading, 6.7)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 11.5)
|
||||||
|
.padding(.vertical, 7.3)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 12.8)
|
||||||
|
.strokeBorder(lineWidth: 1)
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.isShowProfileList = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 13.3)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func NewChatView(scrollToBottom: @escaping () -> Void) -> some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(spacing: 6.7) {
|
||||||
|
Image("ic_bottom_white")
|
||||||
|
Text("새로운 채팅")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.background(Color(hex: "555555").opacity(0.8))
|
||||||
|
.cornerRadius(16.7)
|
||||||
|
.padding(.bottom, 13.3)
|
||||||
|
.onTapGesture { scrollToBottom() }
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func InputChatView(scrollToBottom: @escaping () -> Void) -> some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
TextField("채팅을 입력하세요", text: $viewModel.chatMessage)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.accentColor(Color(hex: "9970ff"))
|
||||||
|
.keyboardType(.default)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("btn_message_send")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 35, height: 35)
|
||||||
|
.padding(6.7)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.sendMessage()
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(Color(hex: "232323"))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.strokeBorder(lineWidth: 1)
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
)
|
||||||
|
.padding(13.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func ChatView() -> some View {
|
||||||
|
LazyVGrid(columns: chatColumns, alignment: .leading, spacing: 20) {
|
||||||
|
ForEach(0..<viewModel.messages.count, id: \.self) { index in
|
||||||
|
switch (viewModel.messages[index].type) {
|
||||||
|
case LiveRoomChatType.DONATION:
|
||||||
|
let chatMessage = viewModel.messages[index] as! LiveRoomDonationChat
|
||||||
|
LiveRoomDonationChatItemView(chatMessage: chatMessage)
|
||||||
|
|
||||||
|
case LiveRoomChatType.JOIN:
|
||||||
|
let chatMessage = viewModel.messages[index] as! LiveRoomJoinChat
|
||||||
|
LiveRoomJoinChatItemView(chatMessage: chatMessage)
|
||||||
|
|
||||||
|
default:
|
||||||
|
let chatMessage = viewModel.messages[index] as! LiveRoomNormalChat
|
||||||
|
LiveRoomChatItemView(
|
||||||
|
chatMessage: chatMessage,
|
||||||
|
onClickProfile: {
|
||||||
|
if chatMessage.userId != UserDefaults.int(forKey: .userId) {
|
||||||
|
viewModel.getUserProfile(userId: chatMessage.userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rotationEffect(Angle(degrees: 180))
|
||||||
|
.valueChanged(value: viewModel.messageChangeFlag) { _ in
|
||||||
|
if viewModel.offset - viewModel.originOffset > (56.7 * 2) {
|
||||||
|
isShowingNewChat = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveRoomView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
LiveRoomView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AttributedTextView: UIViewRepresentable {
|
||||||
|
let attributedString: NSAttributedString
|
||||||
|
let lineLimit: Int
|
||||||
|
let onURLTapped: (URL) -> Void
|
||||||
|
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UITextView {
|
||||||
|
let textView = UITextView()
|
||||||
|
textView.isEditable = false
|
||||||
|
textView.isSelectable = false
|
||||||
|
textView.isScrollEnabled = false
|
||||||
|
textView.backgroundColor = .clear
|
||||||
|
textView.font = UIFont(name: Font.light.rawValue, size: 11.3)
|
||||||
|
textView.textColor = .white
|
||||||
|
textView.textContainer.lineFragmentPadding = 0
|
||||||
|
textView.textContainerInset = .zero
|
||||||
|
textView.textContainer.maximumNumberOfLines = lineLimit
|
||||||
|
textView.textContainer.lineBreakMode = lineLimit == 1 ? .byTruncatingTail : .byWordWrapping
|
||||||
|
textView.delegate = context.coordinator
|
||||||
|
|
||||||
|
textView.isUserInteractionEnabled = true
|
||||||
|
|
||||||
|
// Add tap gesture recognizer to handle URL tap events
|
||||||
|
let tapGestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTapGesture(_:)))
|
||||||
|
textView.addGestureRecognizer(tapGestureRecognizer)
|
||||||
|
|
||||||
|
|
||||||
|
return textView
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||||
|
uiView.attributedText = attributedString
|
||||||
|
uiView.textColor = UIColor.white
|
||||||
|
uiView.textContainer.maximumNumberOfLines = lineLimit
|
||||||
|
uiView.textContainer.lineBreakMode = lineLimit == 1 ? .byTruncatingTail : .byWordWrapping
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(onURLTapped: onURLTapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UITextViewDelegate {
|
||||||
|
let onURLTapped: (URL) -> Void
|
||||||
|
let linkAttributeName = NSAttributedString.Key.link.rawValue
|
||||||
|
|
||||||
|
init(onURLTapped: @escaping (URL) -> Void) {
|
||||||
|
self.onURLTapped = onURLTapped
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleTapGesture(_ gesture: UITapGestureRecognizer) {
|
||||||
|
let textView = gesture.view as? UITextView
|
||||||
|
let location = gesture.location(in: textView)
|
||||||
|
|
||||||
|
let layoutManager = textView?.layoutManager
|
||||||
|
let characterIndex = layoutManager?.characterIndex(for: location, in: textView!.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
|
||||||
|
|
||||||
|
if characterIndex != NSNotFound {
|
||||||
|
let attributedString = textView?.attributedText
|
||||||
|
|
||||||
|
attributedString?.enumerateAttribute(NSAttributedString.Key(rawValue: linkAttributeName), in: NSRange(location: 0, length: attributedString!.length), options: []) { value, range, _ in
|
||||||
|
if let url = value as? URL, NSLocationInRange(characterIndex!, range) {
|
||||||
|
onURLTapped(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// SetManagerOrSpeakerOrAudienceRequest.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct SetManagerOrSpeakerOrAudienceRequest: Encodable {
|
||||||
|
let roomId: Int
|
||||||
|
let memberId: Int
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ struct HomeView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = HomeViewModel()
|
@StateObject var viewModel = HomeViewModel()
|
||||||
@StateObject var appState = AppState.shared
|
@StateObject var appState = AppState.shared
|
||||||
|
@StateObject var contentPlayManager = ContentPlayManager.shared
|
||||||
|
|
||||||
private let liveView = LiveView()
|
private let liveView = LiveView()
|
||||||
private let explorer = ExplorerView()
|
private let explorer = ExplorerView()
|
||||||
|
@ -49,6 +50,57 @@ struct HomeView: View {
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
if contentPlayManager.isShowingMiniPlayer {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
KFImage(URL(string: contentPlayManager.coverImage))
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 36.7, height: 36.7)
|
||||||
|
.cornerRadius(5.3)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2.3) {
|
||||||
|
Text(contentPlayManager.title)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.lineLimit(2)
|
||||||
|
|
||||||
|
Text(contentPlayManager.nickname)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 11))
|
||||||
|
.foregroundColor(Color(hex: "d2d2d2"))
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 10.7)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(contentPlayManager.isPlaying ? "ic_noti_pause" : "btn_bar_play")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 25, height: 25)
|
||||||
|
.onTapGesture {
|
||||||
|
if contentPlayManager.isPlaying {
|
||||||
|
contentPlayManager.pauseAudio()
|
||||||
|
} else {
|
||||||
|
contentPlayManager
|
||||||
|
.playAudio(contentId: contentPlayManager.contentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image("ic_noti_stop")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 25, height: 25)
|
||||||
|
.padding(.leading, 16)
|
||||||
|
.onTapGesture { contentPlayManager.stopAudio() }
|
||||||
|
}
|
||||||
|
.padding(.vertical, 10.7)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
appState
|
||||||
|
.setAppStep(
|
||||||
|
step: .contentDetail(contentId: contentPlayManager.contentId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BottomTabView(width: proxy.size.width, currentTab: $viewModel.currentTab)
|
BottomTabView(width: proxy.size.width, currentTab: $viewModel.currentTab)
|
||||||
|
|
||||||
if proxy.safeAreaInsets.bottom > 0 {
|
if proxy.safeAreaInsets.bottom > 0 {
|
||||||
|
@ -63,6 +115,10 @@ struct HomeView: View {
|
||||||
viewModel.getEventPopup()
|
viewModel.getEventPopup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if appState.isShowPlayer {
|
||||||
|
LiveRoomView()
|
||||||
|
}
|
||||||
|
|
||||||
if appState.isShowNotificationSettingsDialog {
|
if appState.isShowNotificationSettingsDialog {
|
||||||
NotificationSettingsDialog()
|
NotificationSettingsDialog()
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,10 @@ final class UserRepository {
|
||||||
return api.requestPublisher(.getMemberInfo)
|
return api.requestPublisher(.getMemberInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getMemberCan() -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.getMemberInfo)
|
||||||
|
}
|
||||||
|
|
||||||
func updateNotificationSettings(live: Bool? = nil, uploadContent: Bool? = nil, message: Bool? = nil) -> AnyPublisher<Response, MoyaError> {
|
func updateNotificationSettings(live: Bool? = nil, uploadContent: Bool? = nil, message: Bool? = nil) -> AnyPublisher<Response, MoyaError> {
|
||||||
return api.requestPublisher(
|
return api.requestPublisher(
|
||||||
.notification(
|
.notification(
|
||||||
|
|