재생목록 플레이어 추가
This commit is contained in:
parent
9ca1493255
commit
c3e60bd92c
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_player_pause.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_player_pause.imageset/ic_player_pause.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_player_pause.imageset/ic_player_pause.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 542 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_player_play.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_player_play.imageset/ic_player_play.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_player_play.imageset/ic_player_play.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_playlist.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 655 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_skip_back.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 823 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_skip_forward.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_skip_forward.imageset/ic_skip_forward.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_skip_forward.imageset/ic_skip_forward.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 822 B |
|
@ -18,6 +18,7 @@ class AppState: ObservableObject {
|
||||||
didSet {
|
didSet {
|
||||||
if isShowPlayer {
|
if isShowPlayer {
|
||||||
ContentPlayManager.shared.stopAudio()
|
ContentPlayManager.shared.stopAudio()
|
||||||
|
ContentPlayerPlayManager.shared.resetPlayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ enum ContentApi {
|
||||||
case pinContent(contentId: Int)
|
case pinContent(contentId: Int)
|
||||||
case unpinContent(contentId: Int)
|
case unpinContent(contentId: Int)
|
||||||
case getAudioContentByTheme(themeId: Int, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int, sort: ContentAllByThemeViewModel.Sort)
|
case getAudioContentByTheme(themeId: Int, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int, sort: ContentAllByThemeViewModel.Sort)
|
||||||
|
case generateUrl(contentId: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ContentApi: TargetType {
|
extension ContentApi: TargetType {
|
||||||
|
@ -129,6 +130,9 @@ extension ContentApi: TargetType {
|
||||||
|
|
||||||
case .getAudioContentByTheme(let themeId, _, _, _, _, _):
|
case .getAudioContentByTheme(let themeId, _, _, _, _, _):
|
||||||
return "/audio-content/theme/\(themeId)/content"
|
return "/audio-content/theme/\(themeId)/content"
|
||||||
|
|
||||||
|
case .generateUrl(let contentId):
|
||||||
|
return "/audio-content/\(contentId)/generate-url"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +141,7 @@ extension ContentApi: TargetType {
|
||||||
case .getAudioContentList, .getAudioContentDetail, .getOrderList, .getAudioContentThemeList,
|
case .getAudioContentList, .getAudioContentDetail, .getOrderList, .getAudioContentThemeList,
|
||||||
.getAudioContentCommentList, .getAudioContentCommentReplyList, .getNewContentOfTheme,
|
.getAudioContentCommentList, .getAudioContentCommentReplyList, .getNewContentOfTheme,
|
||||||
.getNewContentThemeList, .getNewContentAllOfTheme, .getAudioContentListByCurationId, .getContentRanking,
|
.getNewContentThemeList, .getNewContentAllOfTheme, .getAudioContentListByCurationId, .getContentRanking,
|
||||||
.getContentRankingSortType:
|
.getContentRankingSortType, .generateUrl:
|
||||||
return .get
|
return .get
|
||||||
|
|
||||||
case .getMainBannerList, .getMainOrderList, .getNewContentUploadCreatorList, .getCurationList, .getAudioContentByTheme:
|
case .getMainBannerList, .getMainOrderList, .getNewContentUploadCreatorList, .getCurationList, .getAudioContentByTheme:
|
||||||
|
@ -195,7 +199,7 @@ extension ContentApi: TargetType {
|
||||||
case .addAllPlaybackTracking(let request):
|
case .addAllPlaybackTracking(let request):
|
||||||
return .requestJSONEncodable(request)
|
return .requestJSONEncodable(request)
|
||||||
|
|
||||||
case .getAudioContentThemeList, .getMainBannerList, .getMainOrderList, .getNewContentUploadCreatorList:
|
case .getAudioContentThemeList, .getMainBannerList, .getMainOrderList, .getNewContentUploadCreatorList, .generateUrl:
|
||||||
return .requestPlain
|
return .requestPlain
|
||||||
|
|
||||||
case .uploadAudioContent(let parameters):
|
case .uploadAudioContent(let parameters):
|
||||||
|
|
|
@ -55,6 +55,9 @@ struct ContentDetailPlayView: View {
|
||||||
} else if audioContent.releaseDate == nil && !isAlertPreview || (audioContent.isActivePreview && !audioContent.contentUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) {
|
} else if audioContent.releaseDate == nil && !isAlertPreview || (audioContent.isActivePreview && !audioContent.contentUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) {
|
||||||
Image(isPlaying() ? "btn_audio_content_pause" : isAlertPreview ? "btn_audio_content_preview_play" : "btn_audio_content_play")
|
Image(isPlaying() ? "btn_audio_content_pause" : isAlertPreview ? "btn_audio_content_preview_play" : "btn_audio_content_play")
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
ContentPlayManager.shared.stopAudio()
|
||||||
|
ContentPlayerPlayManager.shared.resetPlayer()
|
||||||
|
|
||||||
if isPlaying() {
|
if isPlaying() {
|
||||||
contentPlayManager.pauseAudio()
|
contentPlayManager.pauseAudio()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
//
|
||||||
|
// AudioContentPlaylistManager.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/16/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class AudioContentPlaylistManager {
|
||||||
|
private var currentIndex = -1
|
||||||
|
|
||||||
|
let playlist: [AudioContentPlaylistContent]
|
||||||
|
|
||||||
|
init(playlist: [AudioContentPlaylistContent]) {
|
||||||
|
self.playlist = playlist
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveToNext() -> AudioContentPlaylistContent? {
|
||||||
|
if !playlist.isEmpty {
|
||||||
|
currentIndex = currentIndex + 1 >= playlist.count ? 0 : currentIndex + 1
|
||||||
|
return playlist[currentIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveToPrevious() -> AudioContentPlaylistContent? {
|
||||||
|
if !playlist.isEmpty {
|
||||||
|
currentIndex = currentIndex - 1 < 0 ? playlist.count - 1 : currentIndex - 1
|
||||||
|
return playlist[currentIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasNextContent() -> Bool {
|
||||||
|
return currentIndex + 1 < playlist.count
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
//
|
||||||
|
// ContentGenerateUrlRepository.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/17/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CombineMoya
|
||||||
|
import Combine
|
||||||
|
import Moya
|
||||||
|
|
||||||
|
class ContentGenerateUrlRepository {
|
||||||
|
private let api = MoyaProvider<ContentApi>()
|
||||||
|
|
||||||
|
func generateUrl(contentId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.generateUrl(contentId: contentId))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,326 @@
|
||||||
|
//
|
||||||
|
// ContentPlayerPlayManager.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/16/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AVKit
|
||||||
|
import MediaPlayer
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class ContentPlayerPlayManager: NSObject, ObservableObject {
|
||||||
|
static let shared = ContentPlayerPlayManager()
|
||||||
|
|
||||||
|
private let repository = ContentGenerateUrlRepository()
|
||||||
|
|
||||||
|
@Published var title = ""
|
||||||
|
@Published var nickname = ""
|
||||||
|
@Published var coverImageUrl = ""
|
||||||
|
@Published var creatorProfileUrl = ""
|
||||||
|
|
||||||
|
@Published private (set) var isShowingMiniPlayer = false
|
||||||
|
@Published private (set) var isPlaying = false
|
||||||
|
|
||||||
|
@Published var isLoading = false
|
||||||
|
@Published var errorMessage = ""
|
||||||
|
@Published var isShowPopup = false
|
||||||
|
@Published var isShowPlaylist = false
|
||||||
|
@Published var playlist: [AudioContentPlaylistContent] = []
|
||||||
|
@Published var currentTime: Double = 0.0
|
||||||
|
@Published private (set) var duration: Double = 0.0
|
||||||
|
@Published var isEditing = false
|
||||||
|
|
||||||
|
var player: AVPlayer?
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@Published var bufferedTime: Double = 0 // 현재 버퍼링된 시간
|
||||||
|
@Published var isPlaybackLikelyToKeepUp: Bool = false // 재생 가능 상태
|
||||||
|
|
||||||
|
let minimumBufferedTime: Double = 5.0
|
||||||
|
|
||||||
|
var playlistManager: AudioContentPlaylistManager? = nil
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
self.player = AVPlayer()
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPlaylist(playlist: [AudioContentPlaylistContent]) {
|
||||||
|
resetPlayer()
|
||||||
|
self.playlist = playlist
|
||||||
|
playlistManager = AudioContentPlaylistManager(playlist: playlist)
|
||||||
|
playNextContent()
|
||||||
|
isShowingMiniPlayer = true
|
||||||
|
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupPlayer(with url: URL) {
|
||||||
|
// 기존 playerItem 관련 Combine 구독 해제
|
||||||
|
cancellables.removeAll()
|
||||||
|
|
||||||
|
// 새로운 AVPlayerItem 생성
|
||||||
|
let playerItem = AVPlayerItem(url: url)
|
||||||
|
self.player?.replaceCurrentItem(with: playerItem)
|
||||||
|
self.player?
|
||||||
|
.periodicTimePublisher(interval: CMTime(seconds: 0.5, preferredTimescale: 600))
|
||||||
|
.sink { [weak self] currentTime in
|
||||||
|
if !(self?.isEditing ?? false) {
|
||||||
|
self?.currentTime = CMTimeGetSeconds(currentTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
playerItem.publisher(for: \.duration)
|
||||||
|
.map { CMTimeGetSeconds($0) }
|
||||||
|
.filter { !$0.isNaN } // NaN 방지
|
||||||
|
.sink { [weak self] duration in
|
||||||
|
self?.duration = duration
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
playerItem.publisher(for: \.loadedTimeRanges)
|
||||||
|
.compactMap { $0.first?.timeRangeValue }
|
||||||
|
.map { CMTimeGetSeconds($0.start) + CMTimeGetSeconds($0.duration) }
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assign(to: &$bufferedTime)
|
||||||
|
|
||||||
|
playerItem.publisher(for: \.isPlaybackLikelyToKeepUp)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assign(to: &$isPlaybackLikelyToKeepUp)
|
||||||
|
|
||||||
|
// CombineLatest로 버퍼링 및 재생 조건 확인
|
||||||
|
Publishers.CombineLatest($bufferedTime, $isPlaybackLikelyToKeepUp)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] bufferedTime, isLikelyToKeepUp in
|
||||||
|
self?.checkPlaybackStart(bufferedTime: bufferedTime, isLikelyToKeepUp: isLikelyToKeepUp)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
// Combine으로 재생 완료 이벤트 감지
|
||||||
|
NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: playerItem)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.handlePlaybackEnded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkPlaybackStart(bufferedTime: Double, isLikelyToKeepUp: Bool) {
|
||||||
|
if bufferedTime >= minimumBufferedTime && isLikelyToKeepUp {
|
||||||
|
if !isPlaying {
|
||||||
|
player?.play()
|
||||||
|
isPlaying = true
|
||||||
|
isLoading = false
|
||||||
|
DEBUG_LOG("재생 시작: \(bufferedTime)초 버퍼링")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if isPlaying {
|
||||||
|
player?.pause()
|
||||||
|
isPlaying = false
|
||||||
|
isLoading = true
|
||||||
|
DEBUG_LOG("재생 중단: 버퍼링 부족 (\(bufferedTime)초)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handlePlaybackEnded() {
|
||||||
|
playNextContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func playNextContent() {
|
||||||
|
stop()
|
||||||
|
|
||||||
|
if let content = playlistManager?.moveToNext() {
|
||||||
|
generateUrl(contentId: content.id) { [unowned self] url in
|
||||||
|
self.urlGenerateSuccess(content: content, url: url)
|
||||||
|
} onFailure: {
|
||||||
|
if let hasNextContent = self.playlistManager?.hasNextContent(), hasNextContent {
|
||||||
|
self.playNextContent()
|
||||||
|
} else {
|
||||||
|
self.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func playPreviousContent() {
|
||||||
|
stop()
|
||||||
|
|
||||||
|
if let content = playlistManager?.moveToPrevious() {
|
||||||
|
generateUrl(contentId: content.id) { [unowned self] url in
|
||||||
|
self.urlGenerateSuccess(content: content, url: url)
|
||||||
|
} onFailure: {
|
||||||
|
if let hasNextContent = self.playlistManager?.hasNextContent(), hasNextContent {
|
||||||
|
self.playNextContent()
|
||||||
|
} else {
|
||||||
|
self.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func urlGenerateSuccess(content: AudioContentPlaylistContent, url: String) {
|
||||||
|
title = content.title
|
||||||
|
nickname = content.creatorNickname
|
||||||
|
coverImageUrl = content.coverUrl
|
||||||
|
creatorProfileUrl = content.creatorProfileUrl
|
||||||
|
setupPlayer(with: URL(string: url)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
func playOrPause() {
|
||||||
|
if player?.timeControlStatus == .paused {
|
||||||
|
player?.play()
|
||||||
|
isPlaying = true
|
||||||
|
} else {
|
||||||
|
player?.pause()
|
||||||
|
isPlaying = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stop() {
|
||||||
|
isPlaying = false
|
||||||
|
isLoading = false
|
||||||
|
player?.pause()
|
||||||
|
player?.seek(to: .zero)
|
||||||
|
duration = 0
|
||||||
|
currentTime = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetPlayer() {
|
||||||
|
stop()
|
||||||
|
isShowingMiniPlayer = false
|
||||||
|
|
||||||
|
title = ""
|
||||||
|
nickname = ""
|
||||||
|
coverImageUrl = ""
|
||||||
|
creatorProfileUrl = ""
|
||||||
|
|
||||||
|
cancellables.removeAll()
|
||||||
|
playlistManager = nil
|
||||||
|
unRegisterRemoteControlEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
func seek(to time: Double) {
|
||||||
|
let cmTime = CMTime(seconds: time, preferredTimescale: 600)
|
||||||
|
player?.seek(to: cmTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateUrl(contentId: Int, onSuccess: @escaping (String) -> Void, onFailure: @escaping () -> Void) {
|
||||||
|
if contentId < 0 {
|
||||||
|
onFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isLoading {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
repository.generateUrl(contentId: contentId)
|
||||||
|
.sink { result in
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
}
|
||||||
|
} receiveValue: { [unowned self] response in
|
||||||
|
self.isLoading = false
|
||||||
|
let responseData = response.data
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonDecoder = JSONDecoder()
|
||||||
|
let decoded = try jsonDecoder.decode(ApiResponse<GenerateUrlResponse>.self, from: responseData)
|
||||||
|
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
onSuccess(data.contentUrl)
|
||||||
|
} else {
|
||||||
|
self.isLoading = false
|
||||||
|
onFailure()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
self.isLoading = false
|
||||||
|
onFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func registerNowPlayingInfoCenter() {
|
||||||
|
let center = MPNowPlayingInfoCenter.default()
|
||||||
|
var nowPlayingInfo = center.nowPlayingInfo ?? [String: Any]()
|
||||||
|
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyTitle] = title
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyArtist] = nickname
|
||||||
|
if let artworkURL = URL(string: coverImageUrl), let imageData = try? Data(contentsOf: artworkURL), let artworkImage = UIImage(data: imageData) {
|
||||||
|
let artwork = MPMediaItemArtwork(boundsSize: artworkImage.size) { size in
|
||||||
|
return artworkImage
|
||||||
|
}
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
|
||||||
|
}
|
||||||
|
|
||||||
|
if let player = player {
|
||||||
|
// 콘텐츠 총 길이
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = player.currentItem?.duration ?? .zero
|
||||||
|
// 콘텐츠 재생 시간에 따른 progressBar 초기화
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate
|
||||||
|
// 콘텐츠 현재 재생시간
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player.currentTime
|
||||||
|
}
|
||||||
|
|
||||||
|
center.nowPlayingInfo = nowPlayingInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
private func registerRemoteControlEvents() {
|
||||||
|
let center = MPRemoteCommandCenter.shared()
|
||||||
|
|
||||||
|
center.playCommand.addTarget { [unowned self] (commandEvent) -> MPRemoteCommandHandlerStatus in
|
||||||
|
self.playOrPause()
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
|
center.pauseCommand.addTarget { [unowned self] (commandEvent) -> MPRemoteCommandHandlerStatus in
|
||||||
|
self.playOrPause()
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
|
center.skipForwardCommand.addTarget { [unowned self] (commandEvent) -> MPRemoteCommandHandlerStatus in
|
||||||
|
self.playNextContent()
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
|
center.skipBackwardCommand.addTarget { [unowned self] (commandEvent) -> MPRemoteCommandHandlerStatus in
|
||||||
|
self.playPreviousContent()
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func unRegisterRemoteControlEvents() {
|
||||||
|
let center = MPRemoteCommandCenter.shared()
|
||||||
|
center.playCommand.removeTarget(nil)
|
||||||
|
center.pauseCommand.removeTarget(nil)
|
||||||
|
center.skipForwardCommand.removeTarget(nil)
|
||||||
|
center.skipBackwardCommand.removeTarget(nil)
|
||||||
|
UIApplication.shared.endReceivingRemoteControlEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AVPlayer {
|
||||||
|
/// AVPlayer의 addPeriodicTimeObserver를 Combine Publisher로 변환
|
||||||
|
func periodicTimePublisher(interval: CMTime, queue: DispatchQueue = .main) -> AnyPublisher<CMTime, Never> {
|
||||||
|
let subject = PassthroughSubject<CMTime, Never>()
|
||||||
|
|
||||||
|
// Periodic Time Observer 추가
|
||||||
|
let timeObserverToken = self.addPeriodicTimeObserver(forInterval: interval, queue: queue) { time in
|
||||||
|
subject.send(time) // 현재 시간을 스트림으로 보냄
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscription이 끝나면 Observer 제거
|
||||||
|
return subject
|
||||||
|
.handleEvents(receiveCancel: { [weak self] in
|
||||||
|
self?.removeTimeObserver(timeObserverToken)
|
||||||
|
})
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
//
|
||||||
|
// ContentPlayerView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/16/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
import Sliders
|
||||||
|
|
||||||
|
struct ContentPlayerView: View {
|
||||||
|
|
||||||
|
@StateObject var playerManager = ContentPlayerPlayManager.shared
|
||||||
|
|
||||||
|
@Binding var isShowing: Bool
|
||||||
|
let playlist: [AudioContentPlaylistContent]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
BaseView(isLoading: $playerManager.isLoading) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("ic_bottom_white")
|
||||||
|
.onTapGesture { isShowing = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(playerManager.title)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 16))
|
||||||
|
.foregroundColor(.grayee)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.top, 16)
|
||||||
|
|
||||||
|
HStack(spacing: 5.3) {
|
||||||
|
KFImage(URL(string: playerManager.creatorProfileUrl))
|
||||||
|
.cancelOnDisappear(true)
|
||||||
|
.downsampling(size: CGSize(width: 26.7, height: 26.7))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 26.7, height: 26.7)
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
Text(playerManager.nickname)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(.gray90)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.top, 21)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if playerManager.isShowPlaylist {
|
||||||
|
ScrollView(.vertical) {
|
||||||
|
LazyVStack(alignment: .leading, spacing: 0) {
|
||||||
|
ForEach(0..<playerManager.playlist.count, id: \.self) {
|
||||||
|
PlaylistContentItemView(item: playerManager.playlist[$0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
KFImage(URL(string: playerManager.coverImageUrl))
|
||||||
|
.cancelOnDisappear(true)
|
||||||
|
.downsampling(size: CGSize(width: 240, height: 240))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 240, height: 240)
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if playerManager.duration > 0 {
|
||||||
|
ValueSlider(
|
||||||
|
value: $playerManager.currentTime,
|
||||||
|
in: 0...playerManager.duration,
|
||||||
|
step: 1.0,
|
||||||
|
onEditingChanged: { editing in
|
||||||
|
playerManager.isEditing = editing
|
||||||
|
if !editing {
|
||||||
|
playerManager.seek(to: playerManager.currentTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.valueSliderStyle(
|
||||||
|
HorizontalValueSliderStyle(
|
||||||
|
track: HorizontalValueTrack(
|
||||||
|
view: Rectangle().foregroundColor(Color.button),
|
||||||
|
mask: Rectangle()
|
||||||
|
)
|
||||||
|
.background(Rectangle().foregroundColor(Color.gray97.opacity(0.3)))
|
||||||
|
.frame(height: 5.3),
|
||||||
|
thumbSize: CGSize(width: 10, height: 10),
|
||||||
|
options: .interactiveTrack
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.frame(height: 11)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text(secondsToMinutesSeconds(seconds: Int(playerManager.currentTime)))
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(.graybb)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text(secondsToMinutesSeconds(seconds: Int(playerManager.duration)))
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(.gray77)
|
||||||
|
}
|
||||||
|
.padding(.top, 5.3)
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Image("ic_skip_back")
|
||||||
|
.onTapGesture {
|
||||||
|
playerManager.playPreviousContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(playerManager.isPlaying ? "ic_player_pause" : "ic_player_play")
|
||||||
|
.onTapGesture {
|
||||||
|
playerManager.playOrPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("ic_skip_forward")
|
||||||
|
.onTapGesture {
|
||||||
|
playerManager.playNextContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 21)
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("ic_playlist")
|
||||||
|
.padding(5)
|
||||||
|
.background(Color.gray33.opacity(playerManager.isShowPlaylist ? 1 : 0))
|
||||||
|
.cornerRadius(playerManager.isShowPlaylist ? 6.7 : 0)
|
||||||
|
.onTapGesture { playerManager.isShowPlaylist.toggle() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
.onAppear {
|
||||||
|
if !playlist.isEmpty {
|
||||||
|
playerManager.setPlaylist(playlist: playlist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func secondsToMinutesSeconds(seconds: Int) -> String {
|
||||||
|
let hours = String(format: "%02d", seconds / 3600)
|
||||||
|
let minute = String(format: "%02d", (seconds % 3600) / 60)
|
||||||
|
let second = String(format: "%02d", seconds % 60)
|
||||||
|
|
||||||
|
return "\(hours):\(minute):\(second)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ContentPlayerView(
|
||||||
|
isShowing: .constant(true),
|
||||||
|
playlist: [
|
||||||
|
AudioContentPlaylistContent(
|
||||||
|
id: 1,
|
||||||
|
title: "안녕하세요 오늘은 커버곡을 들려드리려고 해요",
|
||||||
|
category: "커버곡",
|
||||||
|
coverUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||||
|
duration: "00:30:20",
|
||||||
|
creatorNickname: "유저1",
|
||||||
|
creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||||
|
),
|
||||||
|
|
||||||
|
AudioContentPlaylistContent(
|
||||||
|
id: 2,
|
||||||
|
title: "안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요",
|
||||||
|
category: "커버곡",
|
||||||
|
coverUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||||
|
duration: "00:30:20",
|
||||||
|
creatorNickname: "유저1",
|
||||||
|
creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
//
|
||||||
|
// GenerateUrlResponse.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/17/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
struct GenerateUrlResponse: Decodable {
|
||||||
|
let contentUrl: String
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import Kingfisher
|
||||||
|
|
||||||
struct ContentPlaylistDetailView: View {
|
struct ContentPlaylistDetailView: View {
|
||||||
@StateObject var viewModel = ContentPlaylistDetailViewModel()
|
@StateObject var viewModel = ContentPlaylistDetailViewModel()
|
||||||
|
@StateObject var contentPlayerPlayManager = ContentPlayerPlayManager.shared
|
||||||
|
|
||||||
let playlistId: Int
|
let playlistId: Int
|
||||||
@Binding var isShowing: Bool
|
@Binding var isShowing: Bool
|
||||||
|
@ -17,8 +18,10 @@ struct ContentPlaylistDetailView: View {
|
||||||
|
|
||||||
@State private var isShowPopupMenu = false
|
@State private var isShowPopupMenu = false
|
||||||
@State private var isShowDeleteConfirm = false
|
@State private var isShowDeleteConfirm = false
|
||||||
|
@State private var isShowPlayer = false
|
||||||
|
|
||||||
@State private var isShowModify = false
|
@State private var isShowModify = false
|
||||||
|
@State private var playlist: [AudioContentPlaylistContent] = []
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
|
@ -148,6 +151,9 @@ struct ContentPlaylistDetailView: View {
|
||||||
.cornerRadius(5.3)
|
.cornerRadius(5.3)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
ContentPlayManager.shared.stopAudio()
|
||||||
|
playlist = response.contentList
|
||||||
|
isShowPlayer = true
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack(spacing: 5.3) {
|
HStack(spacing: 5.3) {
|
||||||
|
@ -163,6 +169,9 @@ struct ContentPlaylistDetailView: View {
|
||||||
.cornerRadius(5.3)
|
.cornerRadius(5.3)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
ContentPlayManager.shared.stopAudio()
|
||||||
|
playlist = response.contentList.shuffled()
|
||||||
|
isShowPlayer = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.top, 18)
|
.padding(.top, 18)
|
||||||
|
@ -177,6 +186,57 @@ struct ContentPlaylistDetailView: View {
|
||||||
.padding(.horizontal, 13.3)
|
.padding(.horizontal, 13.3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if contentPlayerPlayManager.isShowingMiniPlayer {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
KFImage(URL(string: contentPlayerPlayManager.coverImageUrl))
|
||||||
|
.cancelOnDisappear(true)
|
||||||
|
.downsampling(
|
||||||
|
size: CGSize(
|
||||||
|
width: 36.7,
|
||||||
|
height: 36.7
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 36.7, height: 36.7)
|
||||||
|
.cornerRadius(5.3)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2.3) {
|
||||||
|
Text(contentPlayerPlayManager.title)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13))
|
||||||
|
.foregroundColor(Color.grayee)
|
||||||
|
.lineLimit(2)
|
||||||
|
|
||||||
|
Text(contentPlayerPlayManager.nickname)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 11))
|
||||||
|
.foregroundColor(Color.grayd2)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 10.7)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(contentPlayerPlayManager.isPlaying ? "ic_noti_pause" : "btn_bar_play")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 25, height: 25)
|
||||||
|
.onTapGesture {
|
||||||
|
contentPlayerPlayManager.playOrPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
Image("ic_noti_stop")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 25, height: 25)
|
||||||
|
.padding(.leading, 16)
|
||||||
|
.onTapGesture { contentPlayerPlayManager.resetPlayer() }
|
||||||
|
}
|
||||||
|
.padding(.vertical, 10.7)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.background(Color.gray22)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
playlist = []
|
||||||
|
isShowPlayer = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -260,6 +320,10 @@ struct ContentPlaylistDetailView: View {
|
||||||
reloadData: $reloadData
|
reloadData: $reloadData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isShowPlayer {
|
||||||
|
ContentPlayerView(isShowing: $isShowPlayer, playlist: playlist)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,15 @@ struct HomeView: View {
|
||||||
@StateObject var liveViewModel = LiveViewModel()
|
@StateObject var liveViewModel = LiveViewModel()
|
||||||
@StateObject var appState = AppState.shared
|
@StateObject var appState = AppState.shared
|
||||||
@StateObject var contentPlayManager = ContentPlayManager.shared
|
@StateObject var contentPlayManager = ContentPlayManager.shared
|
||||||
|
@StateObject var contentPlayerPlayManager = ContentPlayerPlayManager.shared
|
||||||
|
|
||||||
private let liveView = LiveView()
|
private let liveView = LiveView()
|
||||||
private let explorer = ExplorerView()
|
private let explorer = ExplorerView()
|
||||||
private let messageView = MessageView()
|
private let messageView = MessageView()
|
||||||
private let contentView = ContentMainView()
|
private let contentView = ContentMainView()
|
||||||
|
|
||||||
|
@State private var isShowPlayer = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { proxy in
|
GeometryReader { proxy in
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
|
@ -53,6 +56,56 @@ struct HomeView: View {
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
if contentPlayerPlayManager.isShowingMiniPlayer {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
KFImage(URL(string: contentPlayerPlayManager.coverImageUrl))
|
||||||
|
.cancelOnDisappear(true)
|
||||||
|
.downsampling(
|
||||||
|
size: CGSize(
|
||||||
|
width: 36.7,
|
||||||
|
height: 36.7
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 36.7, height: 36.7)
|
||||||
|
.cornerRadius(5.3)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2.3) {
|
||||||
|
Text(contentPlayerPlayManager.title)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13))
|
||||||
|
.foregroundColor(Color.grayee)
|
||||||
|
.lineLimit(2)
|
||||||
|
|
||||||
|
Text(contentPlayerPlayManager.nickname)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 11))
|
||||||
|
.foregroundColor(Color.grayd2)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 10.7)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(contentPlayerPlayManager.isPlaying ? "ic_noti_pause" : "btn_bar_play")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 25, height: 25)
|
||||||
|
.onTapGesture {
|
||||||
|
contentPlayerPlayManager.playOrPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
Image("ic_noti_stop")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 25, height: 25)
|
||||||
|
.padding(.leading, 16)
|
||||||
|
.onTapGesture { contentPlayerPlayManager.resetPlayer() }
|
||||||
|
}
|
||||||
|
.padding(.vertical, 10.7)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.background(Color.gray22)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
isShowPlayer = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if contentPlayManager.isShowingMiniPlayer {
|
if contentPlayManager.isShowingMiniPlayer {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
KFImage(URL(string: contentPlayManager.coverImage))
|
KFImage(URL(string: contentPlayManager.coverImage))
|
||||||
|
@ -70,12 +123,12 @@ struct HomeView: View {
|
||||||
VStack(alignment: .leading, spacing: 2.3) {
|
VStack(alignment: .leading, spacing: 2.3) {
|
||||||
Text(contentPlayManager.title)
|
Text(contentPlayManager.title)
|
||||||
.font(.custom(Font.medium.rawValue, size: 13))
|
.font(.custom(Font.medium.rawValue, size: 13))
|
||||||
.foregroundColor(Color(hex: "eeeeee"))
|
.foregroundColor(Color.grayee)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
|
|
||||||
Text(contentPlayManager.nickname)
|
Text(contentPlayManager.nickname)
|
||||||
.font(.custom(Font.medium.rawValue, size: 11))
|
.font(.custom(Font.medium.rawValue, size: 11))
|
||||||
.foregroundColor(Color(hex: "d2d2d2"))
|
.foregroundColor(Color.grayd2)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 10.7)
|
.padding(.horizontal, 10.7)
|
||||||
|
|
||||||
|
@ -101,7 +154,7 @@ struct HomeView: View {
|
||||||
}
|
}
|
||||||
.padding(.vertical, 10.7)
|
.padding(.vertical, 10.7)
|
||||||
.padding(.horizontal, 13.3)
|
.padding(.horizontal, 13.3)
|
||||||
.background(Color(hex: "222222"))
|
.background(Color.gray22)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
appState
|
appState
|
||||||
|
@ -115,7 +168,7 @@ struct HomeView: View {
|
||||||
|
|
||||||
if proxy.safeAreaInsets.bottom > 0 {
|
if proxy.safeAreaInsets.bottom > 0 {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.foregroundColor(Color(hex: "111111"))
|
.foregroundColor(Color.gray11)
|
||||||
.frame(width: proxy.size.width, height: 15.3)
|
.frame(width: proxy.size.width, height: 15.3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -174,6 +227,10 @@ struct HomeView: View {
|
||||||
AppState.shared.eventPopup = nil
|
AppState.shared.eventPopup = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isShowPlayer {
|
||||||
|
ContentPlayerView(isShowing: $isShowPlayer, playlist: [])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
.valueChanged(value: appState.pushRoomId) { value in
|
.valueChanged(value: appState.pushRoomId) { value in
|
||||||
|
|
|
@ -235,6 +235,8 @@ struct SettingsView: View {
|
||||||
desc: "로그아웃 하시겠어요?",
|
desc: "로그아웃 하시겠어요?",
|
||||||
confirmButtonTitle: "확인",
|
confirmButtonTitle: "확인",
|
||||||
confirmButtonAction: {
|
confirmButtonAction: {
|
||||||
|
ContentPlayManager.shared.stopAudio()
|
||||||
|
ContentPlayerPlayManager.shared.resetPlayer()
|
||||||
viewModel.logout {
|
viewModel.logout {
|
||||||
self.isShowLogoutDialog = false
|
self.isShowLogoutDialog = false
|
||||||
AppState.shared.setAppStep(step: .main)
|
AppState.shared.setAppStep(step: .main)
|
||||||
|
@ -254,15 +256,17 @@ struct SettingsView: View {
|
||||||
desc: "모든 기기에서 로그아웃 하시겠어요?",
|
desc: "모든 기기에서 로그아웃 하시겠어요?",
|
||||||
confirmButtonTitle: "확인",
|
confirmButtonTitle: "확인",
|
||||||
confirmButtonAction: {
|
confirmButtonAction: {
|
||||||
|
ContentPlayManager.shared.stopAudio()
|
||||||
|
ContentPlayerPlayManager.shared.resetPlayer()
|
||||||
viewModel.logoutAllDevice {
|
viewModel.logoutAllDevice {
|
||||||
self.isShowLogoutDialog = false
|
self.isShowLogoutAllDeviceDialog = false
|
||||||
AppState.shared.setAppStep(step: .main)
|
AppState.shared.setAppStep(step: .main)
|
||||||
UserDefaults.reset()
|
UserDefaults.reset()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancelButtonTitle: "취소",
|
cancelButtonTitle: "취소",
|
||||||
cancelButtonAction: {
|
cancelButtonAction: {
|
||||||
self.isShowLogoutDialog = false
|
self.isShowLogoutAllDeviceDialog = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue