parent
c7314cc1d4
commit
9fa1bf9f64
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_heart_pink.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/ic_heart_pink.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/ic_heart_pink.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -37,6 +37,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
||||||
|
|
||||||
@Published var coverImageUrl: String?
|
@Published var coverImageUrl: String?
|
||||||
|
|
||||||
|
@Published var isLoadingLikeHeart = false
|
||||||
@Published var isLoading = false
|
@Published var isLoading = false
|
||||||
@Published var errorMessage = ""
|
@Published var errorMessage = ""
|
||||||
@Published var reportMessage = ""
|
@Published var reportMessage = ""
|
||||||
|
@ -140,6 +141,13 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
||||||
@Published var isShowUesrReportView = false
|
@Published var isShowUesrReportView = false
|
||||||
@Published var isShowProfileReportConfirm = false
|
@Published var isShowProfileReportConfirm = false
|
||||||
@Published var isShowNoChattingConfirm = false
|
@Published var isShowNoChattingConfirm = false
|
||||||
|
@Published var isShowNoticeLikeHeart = false {
|
||||||
|
didSet {
|
||||||
|
if !isShowNoticeLikeHeart {
|
||||||
|
isAvailableLikeHeart = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Published var reportUserId = 0
|
@Published var reportUserId = 0
|
||||||
@Published var reportUserNickname = ""
|
@Published var reportUserNickname = ""
|
||||||
|
@ -190,11 +198,16 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
@Published var selectedMenu: SelectedMenu? = nil
|
@Published var selectedMenu: SelectedMenu? = nil
|
||||||
|
|
||||||
|
@Published var hearts: [Heart] = []
|
||||||
|
|
||||||
var signatureImageUrls = [String]()
|
var signatureImageUrls = [String]()
|
||||||
var signatureList = [LiveRoomDonationResponse]()
|
var signatureList = [LiveRoomDonationResponse]()
|
||||||
var isShowSignatureImage = false
|
var isShowSignatureImage = false
|
||||||
|
|
||||||
var timer: DispatchSourceTimer?
|
var timer: DispatchSourceTimer?
|
||||||
|
var heartTimer: DispatchSourceTimer?
|
||||||
|
|
||||||
|
var isAvailableLikeHeart = false
|
||||||
|
|
||||||
private var blockedMemberIdList = Set<Int>()
|
private var blockedMemberIdList = Set<Int>()
|
||||||
|
|
||||||
|
@ -1803,6 +1816,93 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func likeHeart() {
|
||||||
|
if isAvailableLikeHeart {
|
||||||
|
if !isLoadingLikeHeart {
|
||||||
|
isLoadingLikeHeart = true
|
||||||
|
addHeart()
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [unowned self] in
|
||||||
|
self.isLoadingLikeHeart = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isShowNoticeLikeHeart = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addHeart() {
|
||||||
|
let heart = Heart(
|
||||||
|
id: UUID(),
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
opacity: 1,
|
||||||
|
speed: CGFloat.random(in: 1...3),
|
||||||
|
scale: 0.5,
|
||||||
|
direction: Bool.random() ? "left" : "right"
|
||||||
|
)
|
||||||
|
hearts.append(heart)
|
||||||
|
|
||||||
|
if hearts.count == 1 {
|
||||||
|
startHeartTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateHearts() {
|
||||||
|
for i in (0..<hearts.count).reversed() {
|
||||||
|
hearts[i].offsetY -= hearts[i].speed * 2 // Y축으로 이동
|
||||||
|
hearts[i].opacity -= hearts[i].speed * 0.004444444444 // 투명도 감소
|
||||||
|
hearts[i].scale += 0.0067
|
||||||
|
|
||||||
|
if hearts[i].direction == "left" {
|
||||||
|
hearts[i].offsetX -= 0.8
|
||||||
|
|
||||||
|
if hearts[i].offsetX <= -22 {
|
||||||
|
hearts[i].direction = "right"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hearts[i].offsetX += 0.8
|
||||||
|
|
||||||
|
if hearts[i].offsetX >= 22 {
|
||||||
|
hearts[i].direction = "left"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 화면을 벗어나거나 완전히 사라진 하트는 삭제
|
||||||
|
if hearts[i].scale >= 1 || hearts[i].opacity <= 0 || hearts[i].offsetY < -450 {
|
||||||
|
hearts.remove(at: i)
|
||||||
|
|
||||||
|
if hearts.isEmpty {
|
||||||
|
stopHeartTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최대 하트 개수 제한
|
||||||
|
if hearts.count > 100 {
|
||||||
|
hearts.removeFirst()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startHeartTimer() {
|
||||||
|
if heartTimer == nil {
|
||||||
|
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
|
||||||
|
timer.schedule(deadline: .now(), repeating: 0.033) // 30 FPS
|
||||||
|
timer.setEventHandler { [unowned self] in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.updateHearts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timer.resume()
|
||||||
|
self.heartTimer = timer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopHeartTimer() {
|
||||||
|
heartTimer?.cancel()
|
||||||
|
heartTimer = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LiveRoomViewModel: AgoraRtcEngineDelegate {
|
extension LiveRoomViewModel: AgoraRtcEngineDelegate {
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
//
|
||||||
|
// LiveRoomHeartView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 10/24/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LiveRoomHeartView: View {
|
||||||
|
let heart: Heart
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Image("ic_heart_pink")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 24 * heart.scale, height: 24 * heart.scale) // 크기 조절
|
||||||
|
.shadow(radius: 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
LiveRoomHeartView(
|
||||||
|
heart: Heart(
|
||||||
|
id: UUID(),
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
opacity: 1.0,
|
||||||
|
speed: 1.0,
|
||||||
|
scale: 0.5,
|
||||||
|
direction: "left"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
//
|
||||||
|
// Heart.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 10/24/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Heart: Identifiable {
|
||||||
|
let id: UUID
|
||||||
|
var offsetX: CGFloat // X축 위치
|
||||||
|
var offsetY: CGFloat // Y축 위치
|
||||||
|
var opacity: Double // 투명도
|
||||||
|
var speed: CGFloat // 이동 속도
|
||||||
|
var scale: CGFloat // 크기
|
||||||
|
var direction: String
|
||||||
|
}
|
|
@ -178,12 +178,20 @@ struct LiveRoomViewV2: View {
|
||||||
VStack(alignment: .trailing, spacing: 0) {
|
VStack(alignment: .trailing, spacing: 0) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
LiveRoomRightBottomButton(
|
ZStack(alignment: .bottom) {
|
||||||
imageName: viewModel.isSpeakerMute ? "ic_speaker_off" : "ic_speaker_on",
|
LiveRoomRightBottomButton(
|
||||||
onClick: { viewModel.toggleSpeakerMute() }
|
imageName: viewModel.isSpeakerMute ? "ic_speaker_off" : "ic_speaker_on",
|
||||||
)
|
onClick: { viewModel.toggleSpeakerMute() }
|
||||||
.padding(.bottom, 40)
|
)
|
||||||
.padding(.trailing, 13.3)
|
.padding(.bottom, 40)
|
||||||
|
.padding(.trailing, 13.3)
|
||||||
|
|
||||||
|
ForEach(viewModel.hearts) { heart in
|
||||||
|
LiveRoomHeartView(heart: heart)
|
||||||
|
.offset(x: heart.offsetX, y: heart.offsetY)
|
||||||
|
.opacity(heart.opacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HStack(alignment: .bottom, spacing: 0) {
|
HStack(alignment: .bottom, spacing: 0) {
|
||||||
LiveRoomInputChatView {
|
LiveRoomInputChatView {
|
||||||
|
@ -201,11 +209,18 @@ struct LiveRoomViewV2: View {
|
||||||
imageName: "ic_roulette_settings",
|
imageName: "ic_roulette_settings",
|
||||||
onClick: { viewModel.isShowRouletteSettings = true }
|
onClick: { viewModel.isShowRouletteSettings = true }
|
||||||
)
|
)
|
||||||
} else if liveRoomInfo.creatorId != UserDefaults.int(forKey: .userId) && viewModel.isActiveRoulette {
|
} else {
|
||||||
LiveRoomRightBottomButton(
|
LiveRoomRightBottomButton(
|
||||||
imageName: "ic_roulette",
|
imageName: "ic_heart_pink",
|
||||||
onClick: { viewModel.showRoulette() }
|
onClick: { viewModel.likeHeart() }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if viewModel.isActiveRoulette {
|
||||||
|
LiveRoomRightBottomButton(
|
||||||
|
imageName: "ic_roulette",
|
||||||
|
onClick: { viewModel.showRoulette() }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LiveRoomRightBottomButton(
|
LiveRoomRightBottomButton(
|
||||||
|
@ -414,6 +429,17 @@ struct LiveRoomViewV2: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if viewModel.isShowNoticeLikeHeart {
|
||||||
|
SodaDialog(
|
||||||
|
title: "안내",
|
||||||
|
desc: "'좋아해요'는 유료 후원입니다.\n" +
|
||||||
|
"클릭시 1캔이 소진됩니다.",
|
||||||
|
confirmButtonTitle: "확인"
|
||||||
|
) {
|
||||||
|
viewModel.isShowNoticeLikeHeart = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if viewModel.isShowQuitPopup {
|
if viewModel.isShowQuitPopup {
|
||||||
SodaDialog(
|
SodaDialog(
|
||||||
title: "라이브 나가기",
|
title: "라이브 나가기",
|
||||||
|
|
Loading…
Reference in New Issue