diff --git a/SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/Contents.json new file mode 100644 index 0000000..b296832 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/Contents.json @@ -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 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/ic_heart_pink.png b/SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/ic_heart_pink.png new file mode 100644 index 0000000..5c165ad Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/ic_heart_pink.png differ diff --git a/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift b/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift index 3994a7d..1536de5 100644 --- a/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift +++ b/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift @@ -37,6 +37,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject { @Published var coverImageUrl: String? + @Published var isLoadingLikeHeart = false @Published var isLoading = false @Published var errorMessage = "" @Published var reportMessage = "" @@ -140,6 +141,13 @@ final class LiveRoomViewModel: NSObject, ObservableObject { @Published var isShowUesrReportView = false @Published var isShowProfileReportConfirm = false @Published var isShowNoChattingConfirm = false + @Published var isShowNoticeLikeHeart = false { + didSet { + if !isShowNoticeLikeHeart { + isAvailableLikeHeart = true + } + } + } @Published var reportUserId = 0 @Published var reportUserNickname = "" @@ -190,11 +198,16 @@ final class LiveRoomViewModel: NSObject, ObservableObject { } @Published var selectedMenu: SelectedMenu? = nil + @Published var hearts: [Heart] = [] + var signatureImageUrls = [String]() var signatureList = [LiveRoomDonationResponse]() var isShowSignatureImage = false var timer: DispatchSourceTimer? + var heartTimer: DispatchSourceTimer? + + var isAvailableLikeHeart = false private var blockedMemberIdList = Set() @@ -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..= 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 { diff --git a/SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomHeartView.swift b/SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomHeartView.swift new file mode 100644 index 0000000..854b018 --- /dev/null +++ b/SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomHeartView.swift @@ -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" + ) + ) +} diff --git a/SodaLive/Sources/Live/Room/V2/Heart.swift b/SodaLive/Sources/Live/Room/V2/Heart.swift new file mode 100644 index 0000000..f2e6c32 --- /dev/null +++ b/SodaLive/Sources/Live/Room/V2/Heart.swift @@ -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 +} diff --git a/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift b/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift index a809a1c..d4d49ee 100644 --- a/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift +++ b/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift @@ -178,12 +178,20 @@ struct LiveRoomViewV2: View { VStack(alignment: .trailing, spacing: 0) { Spacer() - LiveRoomRightBottomButton( - imageName: viewModel.isSpeakerMute ? "ic_speaker_off" : "ic_speaker_on", - onClick: { viewModel.toggleSpeakerMute() } - ) - .padding(.bottom, 40) - .padding(.trailing, 13.3) + ZStack(alignment: .bottom) { + LiveRoomRightBottomButton( + imageName: viewModel.isSpeakerMute ? "ic_speaker_off" : "ic_speaker_on", + onClick: { viewModel.toggleSpeakerMute() } + ) + .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) { LiveRoomInputChatView { @@ -201,11 +209,18 @@ struct LiveRoomViewV2: View { imageName: "ic_roulette_settings", onClick: { viewModel.isShowRouletteSettings = true } ) - } else if liveRoomInfo.creatorId != UserDefaults.int(forKey: .userId) && viewModel.isActiveRoulette { + } else { LiveRoomRightBottomButton( - imageName: "ic_roulette", - onClick: { viewModel.showRoulette() } + imageName: "ic_heart_pink", + onClick: { viewModel.likeHeart() } ) + + if viewModel.isActiveRoulette { + LiveRoomRightBottomButton( + imageName: "ic_roulette", + onClick: { viewModel.showRoulette() } + ) + } } LiveRoomRightBottomButton( @@ -414,6 +429,17 @@ struct LiveRoomViewV2: View { ) } + if viewModel.isShowNoticeLikeHeart { + SodaDialog( + title: "안내", + desc: "'좋아해요'는 유료 후원입니다.\n" + + "클릭시 1캔이 소진됩니다.", + confirmButtonTitle: "확인" + ) { + viewModel.isShowNoticeLikeHeart = false + } + } + if viewModel.isShowQuitPopup { SodaDialog( title: "라이브 나가기",