diff --git a/.github/actions/Auto_close_associate_issue/main.js b/.github/actions/Auto_close_associate_issue/main.js index 850877d2f..010c4d8a4 100644 --- a/.github/actions/Auto_close_associate_issue/main.js +++ b/.github/actions/Auto_close_associate_issue/main.js @@ -8,7 +8,7 @@ let pattern = /Resolves: #\d+/; let issueNumber; try { - issueNumber = body.match(pattern)[0].replace("Resolves: #", ""); + issueNumber = body.match(pattern)[0].replace("Resolves: #", "").trim(); } catch { issueNumber = -1; } diff --git a/.github/workflows/AutoCloseIssueByPullRequest.yml b/.github/workflows/AutoCloseIssueByPullRequest.yml index 9408599a8..f2fac78bc 100644 --- a/.github/workflows/AutoCloseIssueByPullRequest.yml +++ b/.github/workflows/AutoCloseIssueByPullRequest.yml @@ -2,9 +2,9 @@ name: Auto close issue when PR is merged on: pull_request_target: + branches-ignore: + - "develop" types: [closed] - branches: - - "!develop" jobs: close-issue: diff --git a/Projects/App/Sources/Application/AppComponent+Base.swift b/Projects/App/Sources/Application/AppComponent+Base.swift index 3410453ac..e1e02f80f 100644 --- a/Projects/App/Sources/Application/AppComponent+Base.swift +++ b/Projects/App/Sources/Application/AppComponent+Base.swift @@ -11,10 +11,6 @@ public extension AppComponent { TextPopupComponent(parent: self) } - var togglePopUpFactory: any TogglePopUpFactory { - TogglePopUpComponent(parent: self) - } - var containSongsFactory: any ContainSongsFactory { ContainSongsComponent(parent: self) } diff --git a/Projects/App/Sources/Application/AppComponent+MyInfo.swift b/Projects/App/Sources/Application/AppComponent+MyInfo.swift index 442aabc5a..9535416f7 100644 --- a/Projects/App/Sources/Application/AppComponent+MyInfo.swift +++ b/Projects/App/Sources/Application/AppComponent+MyInfo.swift @@ -34,4 +34,8 @@ extension AppComponent { var profilePopupFactory: any ProfilePopupFactory { ProfilePopupComponent(parent: self) } + + var playTypeTogglePopupFactory: any PlayTypeTogglePopupFactory { + PlayTypeTogglePopupComponent(parent: self) + } } diff --git a/Projects/App/Sources/Application/AppComponent+Playlist.swift b/Projects/App/Sources/Application/AppComponent+Playlist.swift index 10c52b256..cf39453fb 100644 --- a/Projects/App/Sources/Application/AppComponent+Playlist.swift +++ b/Projects/App/Sources/Application/AppComponent+Playlist.swift @@ -80,9 +80,9 @@ public extension AppComponent { } } - var fetchWmPlaylistDetailUseCase: any FetchWmPlaylistDetailUseCase { + var fetchWMPlaylistDetailUseCase: any FetchWMPlaylistDetailUseCase { shared { - FetchWmPlaylistDetailUseCaseImpl(playlistRepository: playlistRepository) + FetchWMPlaylistDetailUseCaseImpl(playlistRepository: playlistRepository) } } diff --git a/Projects/App/Support/Info.plist b/Projects/App/Support/Info.plist index 8b9cb9383..691640fe1 100644 --- a/Projects/App/Support/Info.plist +++ b/Projects/App/Support/Info.plist @@ -185,5 +185,10 @@ UIUserInterfaceStyle Light + LSApplicationQueriesSchemes + + youtube + youtubemusic + diff --git a/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistDetailResponseDTO.swift b/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistDetailResponseDTO.swift index dbb50ca3d..90d255977 100644 --- a/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistDetailResponseDTO.swift +++ b/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistDetailResponseDTO.swift @@ -29,7 +29,7 @@ public extension ArtistDetailResponseDTO { let enName: String private enum CodingKeys: String, CodingKey { - case krName = "krShort" + case krName = "kr" case enName = "en" } } diff --git a/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistListResponseDTO.swift b/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistListResponseDTO.swift index 98f1ca88a..eec4fdb18 100644 --- a/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistListResponseDTO.swift +++ b/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistListResponseDTO.swift @@ -29,7 +29,7 @@ public extension ArtistListResponseDTO { let enName: String private enum CodingKeys: String, CodingKey { - case krName = "krShort" + case krName = "kr" case enName = "en" } } diff --git a/Projects/Domains/PlaylistDomain/Interface/DataSource/RemotePlaylistDataSource.swift b/Projects/Domains/PlaylistDomain/Interface/DataSource/RemotePlaylistDataSource.swift index 86b7d46bc..b4130c423 100644 --- a/Projects/Domains/PlaylistDomain/Interface/DataSource/RemotePlaylistDataSource.swift +++ b/Projects/Domains/PlaylistDomain/Interface/DataSource/RemotePlaylistDataSource.swift @@ -6,7 +6,7 @@ import SongsDomainInterface public protocol RemotePlaylistDataSource { func fetchRecommendPlaylist() -> Single<[RecommendPlaylistEntity]> func fetchPlaylistDetail(id: String, type: PlaylistType) -> Single - func fetchWmPlaylistDetail(id: String) -> Single + func fetchWMPlaylistDetail(id: String) -> Single func updateTitleAndPrivate(key: String, title: String?, isPrivate: Bool?) -> Completable func createPlaylist(title: String) -> Single func fetchPlaylistSongs(key: String) -> Single<[SongEntity]> diff --git a/Projects/Domains/PlaylistDomain/Interface/Entity/WmPlaylistDetailEntity.swift b/Projects/Domains/PlaylistDomain/Interface/Entity/WMPlaylistDetailEntity.swift similarity index 89% rename from Projects/Domains/PlaylistDomain/Interface/Entity/WmPlaylistDetailEntity.swift rename to Projects/Domains/PlaylistDomain/Interface/Entity/WMPlaylistDetailEntity.swift index 7deabaaef..e435b490a 100644 --- a/Projects/Domains/PlaylistDomain/Interface/Entity/WmPlaylistDetailEntity.swift +++ b/Projects/Domains/PlaylistDomain/Interface/Entity/WMPlaylistDetailEntity.swift @@ -1,7 +1,7 @@ import Foundation import SongsDomainInterface -public struct WmPlaylistDetailEntity: Equatable { +public struct WMPlaylistDetailEntity: Equatable { public init( key: String, title: String, diff --git a/Projects/Domains/PlaylistDomain/Interface/Repository/PlaylistRepository.swift b/Projects/Domains/PlaylistDomain/Interface/Repository/PlaylistRepository.swift index 4c73b5446..c8d0e9eea 100644 --- a/Projects/Domains/PlaylistDomain/Interface/Repository/PlaylistRepository.swift +++ b/Projects/Domains/PlaylistDomain/Interface/Repository/PlaylistRepository.swift @@ -6,7 +6,7 @@ import SongsDomainInterface public protocol PlaylistRepository { func fetchRecommendPlaylist() -> Single<[RecommendPlaylistEntity]> func fetchPlaylistDetail(id: String, type: PlaylistType) -> Single - func fetchWmPlaylistDetail(id: String) -> Single + func fetchWMPlaylistDetail(id: String) -> Single func updateTitleAndPrivate(key: String, title: String?, isPrivate: Bool?) -> Completable func createPlaylist(title: String) -> Single func fetchPlaylistSongs(key: String) -> Single<[SongEntity]> diff --git a/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWMPlaylistDetailUseCase.swift b/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWMPlaylistDetailUseCase.swift new file mode 100644 index 000000000..4f3a3ad50 --- /dev/null +++ b/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWMPlaylistDetailUseCase.swift @@ -0,0 +1,6 @@ +import Foundation +import RxSwift + +public protocol FetchWMPlaylistDetailUseCase { + func execute(id: String) -> Single +} diff --git a/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWmPlaylistDetailUseCase.swift b/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWmPlaylistDetailUseCase.swift deleted file mode 100644 index bc9dfad58..000000000 --- a/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWmPlaylistDetailUseCase.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation -import RxSwift - -public protocol FetchWmPlaylistDetailUseCase { - func execute(id: String) -> Single -} diff --git a/Projects/Domains/PlaylistDomain/Sources/API/PlaylistAPI.swift b/Projects/Domains/PlaylistDomain/Sources/API/PlaylistAPI.swift index f00ea811f..9262992a1 100644 --- a/Projects/Domains/PlaylistDomain/Sources/API/PlaylistAPI.swift +++ b/Projects/Domains/PlaylistDomain/Sources/API/PlaylistAPI.swift @@ -8,7 +8,7 @@ import PlaylistDomainInterface public enum PlaylistAPI { case fetchPlaylistDetail(id: String, type: PlaylistType) // 플리 상세 불러오기 - case fetchWmPlaylistDetail(id: String) // 왁뮤 플리 상세 불러오기 + case fetchWMPlaylistDetail(id: String) // 왁뮤 플리 상세 불러오기 case updateTitleAndPrivate(key: String, title: String?, isPrivate: Bool?) // title and private 업데이트 case createPlaylist(title: String) // 플리 생성 case fetchPlaylistSongs(key: String) // 전체 재생 시 곡 데이터만 가져오기 @@ -36,7 +36,7 @@ extension PlaylistAPI: WMAPI { case let .fetchPlaylistDetail(id: id, type: type): return "/\(id)" - case let .fetchWmPlaylistDetail(id: id): + case let .fetchWMPlaylistDetail(id: id): return "/recommend/\(id)" case let .updateTitleAndPrivate(key: key, _, _): @@ -64,7 +64,7 @@ extension PlaylistAPI: WMAPI { public var method: Moya.Method { switch self { - case .fetchRecommendPlaylist, .fetchPlaylistDetail, .fetchWmPlaylistDetail, .fetchPlaylistSongs, + case .fetchRecommendPlaylist, .fetchPlaylistDetail, .fetchWMPlaylistDetail, .fetchPlaylistSongs, .checkSubscription, .requestPlaylistOwnerID: return .get @@ -85,7 +85,7 @@ extension PlaylistAPI: WMAPI { public var task: Moya.Task { switch self { - case .fetchRecommendPlaylist, .fetchPlaylistDetail, .fetchWmPlaylistDetail, .fetchPlaylistSongs, + case .fetchRecommendPlaylist, .fetchPlaylistDetail, .fetchWMPlaylistDetail, .fetchPlaylistSongs, .subscribePlaylist, .checkSubscription, .requestPlaylistOwnerID: return .requestPlain @@ -125,7 +125,7 @@ extension PlaylistAPI: WMAPI { public var jwtTokenType: JwtTokenType { switch self { - case .fetchRecommendPlaylist, .fetchWmPlaylistDetail: + case .fetchRecommendPlaylist, .fetchWMPlaylistDetail: return .none case let .fetchPlaylistDetail(_, type): diff --git a/Projects/Domains/PlaylistDomain/Sources/DataSource/RemotePlaylistDataSourceImpl.swift b/Projects/Domains/PlaylistDomain/Sources/DataSource/RemotePlaylistDataSourceImpl.swift index a96c06dc0..b53b551a4 100644 --- a/Projects/Domains/PlaylistDomain/Sources/DataSource/RemotePlaylistDataSourceImpl.swift +++ b/Projects/Domains/PlaylistDomain/Sources/DataSource/RemotePlaylistDataSourceImpl.swift @@ -22,9 +22,9 @@ public final class RemotePlaylistDataSourceImpl: BaseRemoteDataSource Single { - request(.fetchWmPlaylistDetail(id: id)) - .map(WmPlaylistDetailResponseDTO.self) + public func fetchWMPlaylistDetail(id: String) -> Single { + request(.fetchWMPlaylistDetail(id: id)) + .map(WMPlaylistDetailResponseDTO.self) .map { $0.toDomain() } } diff --git a/Projects/Domains/PlaylistDomain/Sources/Repository/PlaylistRepositoryImpl.swift b/Projects/Domains/PlaylistDomain/Sources/Repository/PlaylistRepositoryImpl.swift index 04a6418e8..fd21dbf66 100644 --- a/Projects/Domains/PlaylistDomain/Sources/Repository/PlaylistRepositoryImpl.swift +++ b/Projects/Domains/PlaylistDomain/Sources/Repository/PlaylistRepositoryImpl.swift @@ -21,8 +21,8 @@ public final class PlaylistRepositoryImpl: PlaylistRepository { remotePlaylistDataSource.fetchPlaylistDetail(id: id, type: type) } - public func fetchWmPlaylistDetail(id: String) -> Single { - remotePlaylistDataSource.fetchWmPlaylistDetail(id: id) + public func fetchWMPlaylistDetail(id: String) -> Single { + remotePlaylistDataSource.fetchWMPlaylistDetail(id: id) } public func updateTitleAndPrivate(key: String, title: String?, isPrivate: Bool?) -> Completable { diff --git a/Projects/Domains/PlaylistDomain/Sources/ResponseDTO/WmPlaylistDetailResponseDTO.swift b/Projects/Domains/PlaylistDomain/Sources/ResponseDTO/WmPlaylistDetailResponseDTO.swift index b5631a1a3..8a81ac91b 100644 --- a/Projects/Domains/PlaylistDomain/Sources/ResponseDTO/WmPlaylistDetailResponseDTO.swift +++ b/Projects/Domains/PlaylistDomain/Sources/ResponseDTO/WmPlaylistDetailResponseDTO.swift @@ -3,7 +3,7 @@ import PlaylistDomainInterface import SongsDomain import SongsDomainInterface -public struct WmPlaylistDetailResponseDTO: Decodable { +public struct WMPlaylistDetailResponseDTO: Decodable { public let key: String? public let title: String public let songs: [SingleSongResponseDTO]? @@ -19,9 +19,9 @@ public struct WmPlaylistDetailResponseDTO: Decodable { } } -public extension WmPlaylistDetailResponseDTO { - func toDomain() -> WmPlaylistDetailEntity { - WmPlaylistDetailEntity( +public extension WMPlaylistDetailResponseDTO { + func toDomain() -> WMPlaylistDetailEntity { + WMPlaylistDetailEntity( key: key ?? "", title: title, songs: (songs ?? []).map { $0.toDomain() }, diff --git a/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWmPlaylistDetailUseCaseImpl.swift b/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWMPlaylistDetailUseCaseImpl.swift similarity index 60% rename from Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWmPlaylistDetailUseCaseImpl.swift rename to Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWMPlaylistDetailUseCaseImpl.swift index 7c1eb818c..419decf16 100644 --- a/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWmPlaylistDetailUseCaseImpl.swift +++ b/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWMPlaylistDetailUseCaseImpl.swift @@ -2,7 +2,7 @@ import Foundation import PlaylistDomainInterface import RxSwift -public struct FetchWmPlaylistDetailUseCaseImpl: FetchWmPlaylistDetailUseCase { +public struct FetchWMPlaylistDetailUseCaseImpl: FetchWMPlaylistDetailUseCase { private let playlistRepository: any PlaylistRepository public init( @@ -11,7 +11,7 @@ public struct FetchWmPlaylistDetailUseCaseImpl: FetchWmPlaylistDetailUseCase { self.playlistRepository = playlistRepository } - public func execute(id: String) -> Single { - playlistRepository.fetchWmPlaylistDetail(id: id) + public func execute(id: String) -> Single { + playlistRepository.fetchWMPlaylistDetail(id: id) } } diff --git a/Projects/Domains/UserDomain/Interface/Entity/PlaylistEntity.swift b/Projects/Domains/UserDomain/Interface/Entity/PlaylistEntity.swift index 2468db8bf..023dcd975 100644 --- a/Projects/Domains/UserDomain/Interface/Entity/PlaylistEntity.swift +++ b/Projects/Domains/UserDomain/Interface/Entity/PlaylistEntity.swift @@ -8,11 +8,13 @@ public struct PlaylistEntity: Equatable { image: String, songCount: Int, userId: String, + private: Bool, isSelected: Bool = false ) { self.key = key self.title = title self.image = image + self.private = `private` self.isSelected = isSelected self.songCount = songCount self.userId = userId @@ -20,5 +22,5 @@ public struct PlaylistEntity: Equatable { public let key, title, image, userId: String public let songCount: Int - public var isSelected: Bool + public var `private`, isSelected: Bool } diff --git a/Projects/Domains/UserDomain/Sources/ResponseDTO/PlaylistResponseDTO.swift b/Projects/Domains/UserDomain/Sources/ResponseDTO/PlaylistResponseDTO.swift index 249d42c2f..4250207fe 100644 --- a/Projects/Domains/UserDomain/Sources/ResponseDTO/PlaylistResponseDTO.swift +++ b/Projects/Domains/UserDomain/Sources/ResponseDTO/PlaylistResponseDTO.swift @@ -16,6 +16,7 @@ public struct PlaylistResponseDTO: Decodable { public let user: PlaylistResponseDTO.User public let imageUrl: String public let songCount: Int + public let `private`: Bool public struct User: Decodable { public let handle: String @@ -30,7 +31,8 @@ public extension PlaylistResponseDTO { title: title, image: imageUrl, songCount: songCount, - userId: user.handle + userId: user.handle, + private: `private` ) } } diff --git a/Projects/Domains/UserDomain/Testing/FetchPlaylistUseCaseStub.swift b/Projects/Domains/UserDomain/Testing/FetchPlaylistUseCaseStub.swift index 530fe9e9f..eb31b2137 100644 --- a/Projects/Domains/UserDomain/Testing/FetchPlaylistUseCaseStub.swift +++ b/Projects/Domains/UserDomain/Testing/FetchPlaylistUseCaseStub.swift @@ -4,15 +4,36 @@ import UserDomainInterface public struct FetchPlaylistUseCaseStub: FetchPlaylistUseCase { let items: [PlaylistEntity] = [ - .init(key: "123", title: "우중충한 장마철 여름에 듣기 좋은 일본 시티팝 플레이리스트", image: "", songCount: 0, userId: ""), - .init(key: "1234", title: "비내리는 도시, 세련된 무드 감각적인 팝송☔️ 분위기 있는 노래 모음", image: "", songCount: 1, userId: ""), - .init(key: "1424", title: "[𝐏𝐥𝐚𝐲𝐥𝐢𝐬𝐭] 여름 밤, 퇴근길에 꽂는 플레이리스트🚃", image: "", songCount: 200, userId: ""), + .init( + key: "123", + title: "우중충한 장마철 여름에 듣기 좋은 일본 시티팝 플레이리스트", + image: "", + songCount: 0, + userId: "", + private: true + ), + .init( + key: "1234", + title: "비내리는 도시, 세련된 무드 감각적인 팝송☔️ 분위기 있는 노래 모음", + image: "", + songCount: 1, + userId: "", + private: true + ), + .init( + key: "1424", + title: "[𝐏𝐥𝐚𝐲𝐥𝐢𝐬𝐭] 여름 밤, 퇴근길에 꽂는 플레이리스트🚃", + image: "", + songCount: 200, + userId: "", + private: false + ), .init( key: "1324", title: "𝐏𝐥𝐚𝐲𝐥𝐢𝐬𝐭 벌써 여름이야? 내 방을 청량한 캘리포니아 해변으로 신나는 여름 팝송 𝐒𝐮𝐦𝐦𝐞𝐫 𝐢𝐬 𝐜𝐨𝐦𝐢𝐧𝐠 🌴", image: "", songCount: 1000, - userId: "" + userId: "", private: true ) ] diff --git a/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistDetailHeaderViewController.swift b/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistDetailHeaderViewController.swift index 7346553c5..d67386f5b 100644 --- a/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistDetailHeaderViewController.swift +++ b/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistDetailHeaderViewController.swift @@ -51,10 +51,10 @@ class ArtistDetailHeaderViewController: UIViewController, ViewControllerFromStor extension ArtistDetailHeaderViewController { func update(model: ArtistEntity) { self.model = model - let artistName: String = model.krName - let artistEngName: String = model.enName.capitalizingFirstLetter + let artistKrName: String = model.krName + let artistEnName: String = model.enName let artistNameAttributedString = NSMutableAttributedString( - string: artistName + " " + artistEngName, + string: artistKrName + " " + artistEnName, attributes: [ .font: DesignSystemFontFamily.Pretendard.bold.font(size: 24), .foregroundColor: DesignSystemAsset.BlueGrayColor.gray900.color, @@ -62,8 +62,8 @@ extension ArtistDetailHeaderViewController { ] ) - let artistNameRange = (artistNameAttributedString.string as NSString).range(of: artistName) - let artistEngNameRange = (artistNameAttributedString.string as NSString).range(of: artistEngName) + let artistKrNameRange = (artistNameAttributedString.string as NSString).range(of: artistKrName) + let artistEnNameRange = (artistNameAttributedString.string as NSString).range(of: artistEnName) artistNameAttributedString.addAttributes( [ @@ -71,7 +71,7 @@ extension ArtistDetailHeaderViewController { .foregroundColor: DesignSystemAsset.BlueGrayColor.gray900.color.withAlphaComponent(0.6), .kern: -0.5 ], - range: artistEngNameRange + range: artistEnNameRange ) let margin: CGFloat = 104.0 @@ -83,7 +83,7 @@ extension ArtistDetailHeaderViewController { artistNameAttributedString.addAttributes( [.font: DesignSystemFontFamily.Pretendard.bold.font(size: availableWidth >= artistNameWidth ? 24 : 20)], - range: artistNameRange + range: artistKrNameRange ) self.artistNameLabelHeight.constant = diff --git a/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistMusicContentViewController.swift b/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistMusicContentViewController.swift index f3cf28b5e..bd3646483 100644 --- a/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistMusicContentViewController.swift +++ b/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistMusicContentViewController.swift @@ -219,7 +219,9 @@ extension ArtistMusicContentViewController: ArtistMusicCellDelegate { .first(where: { $0.songID == id }) else { return } PlayState.shared.append(item: .init(id: tappedSong.songID, title: tappedSong.title, artist: tappedSong.artist)) - songDetailPresenter.present(id: id) + let playlistIDs = PlayState.shared.currentPlaylist + .map(\.id) + songDetailPresenter.present(ids: playlistIDs, selectedID: tappedSong.songID) } } diff --git a/Projects/Features/BaseFeature/Interface/TogglePopUp/TogglePopUpFactory.swift b/Projects/Features/BaseFeature/Interface/TogglePopUp/TogglePopUpFactory.swift deleted file mode 100644 index 87e64942d..000000000 --- a/Projects/Features/BaseFeature/Interface/TogglePopUp/TogglePopUpFactory.swift +++ /dev/null @@ -1,38 +0,0 @@ -import UIKit - -public protocol TogglePopUpFactory { - func makeView( - titleString: String, - firstItemString: String, - secondItemString: String, - cancelButtonText: String, - confirmButtonText: String, - descriptionText: String, - completion: (() -> Void)?, - cancelCompletion: (() -> Void)? - ) -> UIViewController -} - -public extension TogglePopUpFactory { - func makeView( - titleString: String, - firstItemString: String, - secondItemString: String, - cancelButtonText: String = "취소", - confirmButtonText: String = "확인", - descriptionText: String = "", - completion: (() -> Void)? = nil, - cancelCompletion: (() -> Void)? = nil - ) -> UIViewController { - self.makeView( - titleString: titleString, - firstItemString: firstItemString, - secondItemString: secondItemString, - cancelButtonText: cancelButtonText, - confirmButtonText: confirmButtonText, - descriptionText: descriptionText, - completion: completion, - cancelCompletion: cancelCompletion - ) - } -} diff --git a/Projects/Features/BaseFeature/Sources/Components/TogglePopUpComponent.swift b/Projects/Features/BaseFeature/Sources/Components/TogglePopUpComponent.swift deleted file mode 100644 index 363e99582..000000000 --- a/Projects/Features/BaseFeature/Sources/Components/TogglePopUpComponent.swift +++ /dev/null @@ -1,27 +0,0 @@ -import BaseFeatureInterface -import NeedleFoundation -import UIKit - -public final class TogglePopUpComponent: Component, TogglePopUpFactory { - public func makeView( - titleString: String, - firstItemString: String, - secondItemString: String, - cancelButtonText: String = "취소", - confirmButtonText: String = "확인", - descriptionText: String = "", - completion: (() -> Void)? = nil, - cancelCompletion: (() -> Void)? = nil - ) -> UIViewController { - return TogglePopupViewController( - titleString: titleString, - firstItemString: firstItemString, - secondItemString: secondItemString, - cancelButtonText: cancelButtonText, - confirmButtonText: confirmButtonText, - descriptionText: descriptionText, - completion: completion, - cancelCompletion: cancelCompletion - ) - } -} diff --git a/Projects/Features/BaseFeature/Sources/ViewControllers/TogglePopupViewController.swift b/Projects/Features/BaseFeature/Sources/ViewControllers/TogglePopupViewController.swift deleted file mode 100644 index 1f33b0789..000000000 --- a/Projects/Features/BaseFeature/Sources/ViewControllers/TogglePopupViewController.swift +++ /dev/null @@ -1,230 +0,0 @@ -import DesignSystem -import SnapKit -import Then -import UIKit -import Utility - -public final class TogglePopupViewController: UIViewController { - private let dimmView = UIView().then { - $0.backgroundColor = .black.withAlphaComponent(0.4) - } - - private let contentView = UIView().then { - $0.layer.cornerRadius = 24 - $0.backgroundColor = .white - } - - private let titleLabel = WMLabel( - text: "", - textColor: DesignSystemAsset.BlueGrayColor.gray900.color, - font: .t2(weight: .bold), - alignment: .center, - lineHeight: UIFont.WMFontSystem.t2().lineHeight, - kernValue: -0.5 - ) - - private let firstItemButton = UIButton() - - private let secondItemButton = UIButton() - - private let dotImageView = UIImageView().then { - $0.image = DesignSystemAsset.MyInfo.dot.image - } - - private let descriptionLabel = WMLabel( - text: "", - textColor: DesignSystemAsset.BlueGrayColor.gray500.color, - font: .t7(weight: .light), - alignment: .left, - lineHeight: UIFont.WMFontSystem.t7().lineHeight, - kernValue: -0.5 - ) - - private let stackView = UIStackView().then { - $0.axis = .horizontal - $0.spacing = 0 - $0.distribution = .fillEqually - } - - private let cancelButton = UIButton().then { - let cancleButtonBackgroundColor = DesignSystemAsset.BlueGrayColor.blueGray400.color - $0.setBackgroundColor(cancleButtonBackgroundColor, for: .normal) - $0.setTitle("취소", for: .normal) - $0.setTitleColor(DesignSystemAsset.BlueGrayColor.blueGray25.color, for: .normal) - $0.titleLabel?.font = .setFont(.t4(weight: .medium)) - $0.titleLabel?.setTextWithAttributes(alignment: .center) - } - - private let confirmButton = UIButton().then { - let confirmButtonBackgroundColor = DesignSystemAsset.PrimaryColorV2.point.color - $0.setBackgroundColor(confirmButtonBackgroundColor, for: .normal) - $0.setTitle("확인", for: .normal) - $0.setTitleColor(DesignSystemAsset.BlueGrayColor.blueGray25.color, for: .normal) - $0.titleLabel?.font = .setFont(.t4(weight: .medium)) - $0.titleLabel?.setTextWithAttributes(alignment: .center) - } - - var titleString: String = "" - var firstItemString: String = "" - var secondItemString: String = "" - var cancelButtonText: String = "" - var confirmButtonText: String = "" - var descriptionText: String = "" - var completion: (() -> Void)? - var cancelCompletion: (() -> Void)? - - init( - titleString: String, - firstItemString: String, - secondItemString: String, - cancelButtonText: String = "취소", - confirmButtonText: String = "확인", - descriptionText: String = "", - completion: (() -> Void)? = nil, - cancelCompletion: (() -> Void)? = nil - ) { - super.init(nibName: nil, bundle: nil) - self.titleString = titleString - self.firstItemString = firstItemString - self.secondItemString = secondItemString - self.cancelButtonText = cancelButtonText - self.confirmButtonText = confirmButtonText - self.descriptionText = descriptionText - self.completion = completion - self.cancelCompletion = cancelCompletion - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func viewDidLoad() { - super.viewDidLoad() - addViews() - setLayout() - configureUI() - firstItemButton.addTarget(nil, action: #selector(firstItemDidTap), for: .touchUpInside) - secondItemButton.addTarget(nil, action: #selector(secondItemDidTap), for: .touchUpInside) - cancelButton.addTarget(nil, action: #selector(cancelButtonDidTap), for: .touchUpInside) - confirmButton.addTarget(nil, action: #selector(confirmButtonDidTap), for: .touchUpInside) - } - - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - contentView.transform = CGAffineTransform(translationX: 0, y: self.view.frame.height) - UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: { - self.contentView.transform = CGAffineTransform.identity - }, completion: nil) - } - - @objc func firstItemDidTap() { - print("1") - } - - @objc func secondItemDidTap() { - print("2") - } - - @objc func cancelButtonDidTap() { - dismiss() - } - - @objc func confirmButtonDidTap() { - print("confirm") - } -} - -private extension TogglePopupViewController { - func addViews() { - self.view.addSubviews( - dimmView, - contentView - ) - contentView.addSubviews( - titleLabel, - firstItemButton, - secondItemButton, - dotImageView, - descriptionLabel, - stackView - ) - stackView.addArrangedSubviews(cancelButton, confirmButton) - } - - func setLayout() { - dimmView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - contentView.snp.makeConstraints { - $0.width.equalTo(335) - $0.height.equalTo(322) - $0.center.equalToSuperview() - } - - titleLabel.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview().inset(20) - $0.top.equalToSuperview().offset(30) - } - - firstItemButton.snp.makeConstraints { - $0.height.equalTo(60) - $0.top.equalTo(titleLabel.snp.bottom).offset(20) - $0.horizontalEdges.equalToSuperview().inset(20) - } - - secondItemButton.snp.makeConstraints { - $0.height.equalTo(60) - $0.top.equalTo(firstItemButton.snp.bottom).offset(8) - $0.horizontalEdges.equalToSuperview().inset(20) - } - - dotImageView.snp.makeConstraints { - $0.centerY.equalTo(descriptionLabel.snp.centerY) - $0.left.equalToSuperview().offset(20) - } - descriptionLabel.snp.makeConstraints { - $0.top.equalTo(secondItemButton.snp.bottom).offset(8) - $0.left.equalTo(dotImageView.snp.right) - $0.right.equalToSuperview().inset(20) - } - - stackView.snp.makeConstraints { - $0.height.equalTo(56) - $0.horizontalEdges.equalToSuperview() - $0.bottom.equalToSuperview() - } - } - - func configureUI() { - self.view.backgroundColor = .clear - contentView.clipsToBounds = true - - titleLabel.text = self.titleString - firstItemButton.setTitle(self.firstItemString, for: .normal) - secondItemButton.setTitle(self.secondItemString, for: .normal) - descriptionLabel.text = self.descriptionText - cancelButton.setTitle(self.cancelButtonText, for: .normal) - confirmButton.setTitle(self.confirmButtonText, for: .normal) - - let gesture = UITapGestureRecognizer(target: self, action: #selector(tappedAround(_:))) - dimmView.addGestureRecognizer(gesture) - dimmView.isUserInteractionEnabled = true - } - - @objc func tappedAround(_ sender: UITapGestureRecognizer) { - dismiss() - } - - func dismiss() { - UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: { - self.contentView.transform = CGAffineTransform(translationX: 0, y: self.view.frame.height) - }, completion: nil) - // 내려가는 애니메이션이 끝난 다음 dismiss 하기 위해 0.3초 딜레이 - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { - self.dismiss(animated: false) - } - } -} diff --git a/Projects/Features/ChartFeature/Sources/ViewContrillers/ChartContentViewController.swift b/Projects/Features/ChartFeature/Sources/ViewContrillers/ChartContentViewController.swift index eebb01120..203be54d2 100644 --- a/Projects/Features/ChartFeature/Sources/ViewContrillers/ChartContentViewController.swift +++ b/Projects/Features/ChartFeature/Sources/ViewContrillers/ChartContentViewController.swift @@ -196,7 +196,9 @@ extension ChartContentViewController: ChartContentTableViewCellDelegate { .first(where: { $0.id == id }) else { return } PlayState.shared.append(item: .init(id: tappedSong.id, title: tappedSong.title, artist: tappedSong.artist)) - songDetailPresenter.present(id: id) + let playlistIDs = PlayState.shared.currentPlaylist + .map(\.id) + songDetailPresenter.present(ids: playlistIDs, selectedID: tappedSong.id) } } diff --git a/Projects/Features/CreditSongListFeature/Demo/Sources/AppDelegate.swift b/Projects/Features/CreditSongListFeature/Demo/Sources/AppDelegate.swift index 416c17b03..6c7849559 100644 --- a/Projects/Features/CreditSongListFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Features/CreditSongListFeature/Demo/Sources/AppDelegate.swift @@ -1,8 +1,10 @@ +import BaseFeature import BaseFeatureInterface import CreditDomainTesting @testable import CreditSongListFeature import CreditSongListFeatureInterface import Inject +import RxSwift import SignInFeatureInterface import UIKit @@ -65,6 +67,7 @@ final class FakeCreditSongListTabItemFactory: CreditSongListTabItemFactory { let reactor = CreditSongListTabItemReactor( workerName: workerName, creditSortType: sortType, + songDetailPresenter: DummySongDetailPresenter(), fetchCreditSongListUseCase: fetchCreditSongListUseCase ) return Inject.ViewControllerHost( @@ -78,6 +81,16 @@ final class FakeCreditSongListTabItemFactory: CreditSongListTabItemFactory { } } +final class DummySongDetailPresenter: SongDetailPresentable { + var presentSongDetailObservable: RxSwift.Observable<(ids: [String], selectedID: String)> { + .empty() + } + + func present(id: String) {} + + func present(ids: [String], selectedID: String) {} +} + final class DummyContainSongsFactory: ContainSongsFactory { func makeView(songs: [String]) -> UIViewController { let viewController = UIViewController() diff --git a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/Component/CreditSongListTabItemComponent.swift b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/Component/CreditSongListTabItemComponent.swift index 54b79a5a0..b619de867 100644 --- a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/Component/CreditSongListTabItemComponent.swift +++ b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/Component/CreditSongListTabItemComponent.swift @@ -1,3 +1,4 @@ +import BaseFeature import BaseFeatureInterface import CreditDomainInterface import CreditSongListFeatureInterface @@ -10,6 +11,7 @@ public protocol CreditSongListTabItemDependency: Dependency { var containSongsFactory: any ContainSongsFactory { get } var textPopupFactory: any TextPopupFactory { get } var signInFactory: any SignInFactory { get } + var songDetailPresenter: any SongDetailPresentable { get } } public final class CreditSongListTabItemComponent: Component, @@ -18,6 +20,7 @@ public final class CreditSongListTabItemComponent: Component Void) case signIn + case dismiss(completion: () -> Void) } struct State { @@ -55,16 +57,19 @@ final class CreditSongListTabItemReactor: Reactor { let initialState: State private let workerName: String private let creditSortType: CreditSongSortType + private let songDetailPresenter: any SongDetailPresentable private let fetchCreditSongListUseCase: any FetchCreditSongListUseCase init( workerName: String, creditSortType: CreditSongSortType, + songDetailPresenter: any SongDetailPresentable, fetchCreditSongListUseCase: any FetchCreditSongListUseCase ) { self.initialState = .init() self.workerName = workerName self.creditSortType = creditSortType + self.songDetailPresenter = songDetailPresenter self.fetchCreditSongListUseCase = fetchCreditSongListUseCase } @@ -74,6 +79,12 @@ final class CreditSongListTabItemReactor: Reactor { return viewDidLoad() case let .songDidTap(id): return songDidTap(id: id) + case let .songThumbnailDidTap(id): + return navigateMutation(navigateType: .dismiss(completion: { [songDetailPresenter] in + let playlistIDs = PlayState.shared.currentPlaylist + .map(\.id) + songDetailPresenter.present(ids: playlistIDs, selectedID: id) + })) case .randomPlayButtonDidTap: return randomPlayButtonDidTap() case .allSelectButtonDidTap: diff --git a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabItemViewController.swift b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabItemViewController.swift index 521678215..dfa22de51 100644 --- a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabItemViewController.swift +++ b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabItemViewController.swift @@ -40,6 +40,9 @@ final class CreditSongListTabItemViewController: > { [reactor] cell, _, model in let isSelected = reactor?.currentState.selectedSongs.contains(model.id) ?? false cell.update(model, isSelected: isSelected) + cell.setThumbnailTapHandler { [reactor, id = model.id] in + reactor?.action.onNext(.songThumbnailDidTap(id: id)) + } } private lazy var creditSongHeaderRegistration = UICollectionView @@ -159,6 +162,8 @@ final class CreditSongListTabItemViewController: owner.presentTextPopup(text: text, completion: completion) case .signIn: owner.presentSignIn() + case let .dismiss(completion): + owner.dismiss(completion: completion) } } .disposed(by: disposeBag) @@ -309,6 +314,10 @@ extension CreditSongListTabItemViewController { viewController.modalPresentationStyle = .overFullScreen UIApplication.topVisibleViewController()?.present(viewController, animated: true) } + + private func dismiss(completion: @escaping () -> Void) { + UIApplication.keyRootViewController?.dismiss(animated: true, completion: completion) + } } private extension CGFloat { diff --git a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabViewController.swift b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabViewController.swift index df822de88..b5971f7a4 100644 --- a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabViewController.swift +++ b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabViewController.swift @@ -82,11 +82,12 @@ private extension CreditSongListTabViewController { button.selectedFont = DesignSystemFontFamily.Pretendard.bold.font(size: 16) } - bar.indicator.weight = .custom(value: 3) + bar.indicator.weight = .custom(value: 2) bar.indicator.tintColor = DesignSystemAsset.PrimaryColorV2.point.color bar.indicator.overscrollBehavior = .compress addBar(bar, dataSource: self, at: .custom(view: tabContainerView, layout: nil)) + bar.layer.zPosition = 1 } } diff --git a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/CreditSongCollectionViewCell.swift b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/CreditSongCollectionViewCell.swift index 6fd81b0c6..d3f08f864 100644 --- a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/CreditSongCollectionViewCell.swift +++ b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/CreditSongCollectionViewCell.swift @@ -1,6 +1,8 @@ import DesignSystem import Kingfisher import Lottie +import RxGesture +import RxSwift import SnapKit import SongsDomainInterface import Then @@ -45,10 +47,14 @@ final class CreditSongCollectionViewCell: UICollectionViewCell { $0.lineBreakMode = .byTruncatingTail } + private let disposeBag = DisposeBag() + private var handler: (() -> Void)? + override init(frame: CGRect) { super.init(frame: frame) addViews() setLayout() + bind() } @available(*, unavailable) @@ -56,6 +62,10 @@ final class CreditSongCollectionViewCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } + func setThumbnailTapHandler(_ handler: @escaping () -> Void) { + self.handler = handler + } + func update(_ model: CreditSongModel, isSelected: Bool) { self.thumbnailImageView.kf.setImage( with: URL(string: WMImageAPI.fetchYoutubeThumbnail(id: model.id).toString), @@ -99,4 +109,13 @@ private extension CreditSongCollectionViewCell { $0.trailing.equalToSuperview().inset(20) } } + + func bind() { + thumbnailImageView.rx.tapGesture() + .when(.recognized) + .bind { [weak self] _ in + self?.handler?() + } + .disposed(by: disposeBag) + } } diff --git a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/RandomPlayButton.swift b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/RandomPlayButton.swift index e52944b5c..8fe971979 100644 --- a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/RandomPlayButton.swift +++ b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/RandomPlayButton.swift @@ -42,7 +42,7 @@ final class RandomPlayButton: UIButton { randomImageView.snp.makeConstraints { $0.centerY.equalToSuperview() $0.leading.equalToSuperview().inset(32) - $0.size.equalTo(24) + $0.size.equalTo(32) } playLabel.snp.makeConstraints { diff --git a/Projects/Features/HomeFeature/Sources/ViewControllers/HomeViewController.swift b/Projects/Features/HomeFeature/Sources/ViewControllers/HomeViewController.swift index 00a29a596..92bfdd693 100644 --- a/Projects/Features/HomeFeature/Sources/ViewControllers/HomeViewController.swift +++ b/Projects/Features/HomeFeature/Sources/ViewControllers/HomeViewController.swift @@ -410,7 +410,9 @@ extension HomeViewController: HomeChartCellDelegate { func thumbnailDidTap(model: ChartRankingEntity) { LogManager.analytics(HomeAnalyticsLog.clickMusicItem(location: .homeTop100, id: model.id)) PlayState.shared.append(item: .init(id: model.id, title: model.title, artist: model.artist)) - songDetailPresenter.present(id: model.id) + let playlistIDs = PlayState.shared.currentPlaylist + .map(\.id) + songDetailPresenter.present(ids: playlistIDs, selectedID: model.id) } func playButtonDidTap(model: ChartRankingEntity) { @@ -426,7 +428,9 @@ extension HomeViewController: HomeNewSongCellDelegate { func thumbnailDidTap(model: NewSongsEntity) { LogManager.analytics(HomeAnalyticsLog.clickMusicItem(location: .homeRecent, id: model.id)) PlayState.shared.append(item: .init(id: model.id, title: model.title, artist: model.artist)) - songDetailPresenter.present(id: model.id) + let playlistIDs = PlayState.shared.currentPlaylist + .map(\.id) + songDetailPresenter.present(ids: playlistIDs, selectedID: model.id) } func playButtonDidTap(model: NewSongsEntity) { diff --git a/Projects/Features/HomeFeature/Sources/ViewControllers/NewSongsContentViewController.swift b/Projects/Features/HomeFeature/Sources/ViewControllers/NewSongsContentViewController.swift index 7be7c8171..f275a9c73 100644 --- a/Projects/Features/HomeFeature/Sources/ViewControllers/NewSongsContentViewController.swift +++ b/Projects/Features/HomeFeature/Sources/ViewControllers/NewSongsContentViewController.swift @@ -221,7 +221,9 @@ extension NewSongsContentViewController: NewSongsCellDelegate { .first(where: { $0.id == id }) else { return } PlayState.shared.append(item: .init(id: tappedSong.id, title: tappedSong.title, artist: tappedSong.artist)) - songDetailPresenter.present(id: id) + let playlistIDs = PlayState.shared.currentPlaylist + .map(\.id) + songDetailPresenter.present(ids: playlistIDs, selectedID: id) } } diff --git a/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController+Bind.swift b/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController+Bind.swift index 8c6651f5d..2fe1590a3 100644 --- a/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController+Bind.swift +++ b/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController+Bind.swift @@ -8,6 +8,16 @@ extension LyricHighlightingViewController { func inputBind() { input.fetchLyric.onNext(()) + activateButton.rx.tap + .do(onNext: { [activateContentView] _ in + UIView.animate(withDuration: 0.2) { + activateContentView.isHidden = true + } + }) + .map { _ in true } + .bind(to: input.didTapActivateButton) + .disposed(by: disposeBag) + collectionView.rx.itemSelected .bind(to: input.didTapHighlighting) .disposed(by: disposeBag) @@ -39,11 +49,11 @@ extension LyricHighlightingViewController { output.dataSource .skip(1) - .do(onNext: { [indicator, warningView, collectionView, writerLabel] model in - indicator.stopAnimating() + .do(onNext: { [activityIndicator, warningView, collectionView, bottomContentStackView] model in + activityIndicator.stopAnimating() warningView.isHidden = !model.isEmpty - collectionView.isHidden = !warningView.isHidden - writerLabel.isHidden = !warningView.isHidden + collectionView.isHidden = model.isEmpty + bottomContentStackView.isHidden = model.isEmpty }) .bind(to: collectionView.rx.items) { collectionView, index, entity in guard let cell = collectionView.dequeueReusableCell( @@ -58,8 +68,11 @@ extension LyricHighlightingViewController { .disposed(by: disposeBag) output.isStorable - .map { !$0 } - .bind(to: completeButton.rx.isHidden) + .bind(with: self) { owner, isStorable in + UIView.animate(withDuration: 0.2) { + owner.completeButton.alpha = isStorable ? 1 : 0 + } + } .disposed(by: disposeBag) output.goDecoratingScene diff --git a/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController.swift b/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController.swift index f3bb05cec..7ee2609d0 100644 --- a/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController.swift +++ b/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController.swift @@ -48,6 +48,14 @@ public final class LyricHighlightingViewController: UIViewController { $0.backgroundColor = .clear } + let bottomContentStackView = UIStackView().then { + $0.axis = .vertical + $0.distribution = .fill + $0.isHidden = true + } + + let writerContentView = UIView() + let writerLabel = WMLabel( text: "", textColor: .white.withAlphaComponent(0.5), @@ -56,6 +64,19 @@ public final class LyricHighlightingViewController: UIViewController { kernValue: -0.5 ) + let activateContentView = UIView() + + let activateTopLineLabel = UILabel().then { + $0.backgroundColor = DesignSystemAsset.NewGrayColor.gray700.color + } + + let activateButton = UIButton(type: .system).then { + $0.setImage( + DesignSystemAsset.LyricHighlighting.lyricHighlightSaveOff.image.withRenderingMode(.alwaysOriginal), + for: .normal + ) + } + let warningView = UIView().then { $0.isHidden = true } @@ -85,10 +106,10 @@ public final class LyricHighlightingViewController: UIViewController { $0.setTitleColor(DesignSystemAsset.PrimaryColorV2.point.color, for: .normal) $0.titleLabel?.font = DesignSystemFontFamily.Pretendard.bold.font(size: 12) $0.titleLabel?.setTextWithAttributes(alignment: .center) - $0.isHidden = true + $0.alpha = 0 } - let indicator = NVActivityIndicatorView(frame: .zero).then { + let activityIndicator = NVActivityIndicatorView(frame: .zero).then { $0.type = .circleStrokeSpin $0.color = DesignSystemAsset.PrimaryColorV2.point.color } @@ -151,14 +172,19 @@ private extension LyricHighlightingViewController { thumbnailImageView, dimmedBackgroundView, collectionView, - writerLabel, + bottomContentStackView, warningView, navigationBarView, - indicator + activityIndicator ) + navigationBarView.addSubviews(backButton, navigationTitleStackView, completeButton) navigationTitleStackView.addArrangedSubview(songLabel) navigationTitleStackView.addArrangedSubview(artistLabel) + + bottomContentStackView.addArrangedSubviews(writerContentView, activateContentView) + writerContentView.addSubview(writerLabel) + activateContentView.addSubviews(activateTopLineLabel, activateButton) warningView.addSubviews(warningImageView, warningLabel) } @@ -209,13 +235,36 @@ private extension LyricHighlightingViewController { $0.horizontalEdges.equalToSuperview() } + bottomContentStackView.snp.makeConstraints { + $0.top.equalTo(collectionView.snp.bottom).offset(19) + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalTo(view.safeAreaLayoutGuide) + } + + writerContentView.snp.makeConstraints { + $0.height.equalTo(51) + } + writerLabel.snp.makeConstraints { - $0.top.equalTo(collectionView.snp.bottom).offset(18) + $0.top.equalToSuperview() $0.horizontalEdges.equalToSuperview().inset(20) - $0.bottom.equalTo(view.safeAreaLayoutGuide).offset(-30) $0.height.equalTo(22) } + activateContentView.snp.makeConstraints { + $0.height.equalTo(56) + } + + activateTopLineLabel.snp.makeConstraints { + $0.top.equalToSuperview() + $0.horizontalEdges.equalToSuperview() + $0.height.equalTo(1) + } + + activateButton.snp.makeConstraints { + $0.edges.equalToSuperview() + } + warningView.snp.makeConstraints { $0.top.equalToSuperview().offset(APP_HEIGHT() * ((294.0 - 6.0) / 812.0)) $0.centerX.equalToSuperview() @@ -233,8 +282,8 @@ private extension LyricHighlightingViewController { $0.bottom.equalToSuperview() } - indicator.snp.makeConstraints { - $0.center.equalToSuperview() + activityIndicator.snp.makeConstraints { + $0.center.equalTo(warningView.snp.center) $0.size.equalTo(30) } } @@ -262,7 +311,7 @@ private extension LyricHighlightingViewController { .alternativeSources(alternativeSources) ] ) - indicator.startAnimating() + activityIndicator.startAnimating() } func createLayout() -> UICollectionViewLayout { diff --git a/Projects/Features/LyricHighlightingFeature/Sources/ViewModels/LyricHighlightingViewModel.swift b/Projects/Features/LyricHighlightingFeature/Sources/ViewModels/LyricHighlightingViewModel.swift index 88ed2e459..a23e71a48 100644 --- a/Projects/Features/LyricHighlightingFeature/Sources/ViewModels/LyricHighlightingViewModel.swift +++ b/Projects/Features/LyricHighlightingFeature/Sources/ViewModels/LyricHighlightingViewModel.swift @@ -26,6 +26,7 @@ public final class LyricHighlightingViewModel: ViewModelType { public struct Input { let fetchLyric: PublishSubject = PublishSubject() + let didTapActivateButton: BehaviorRelay = BehaviorRelay(value: false) let didTapHighlighting: BehaviorRelay = BehaviorRelay(value: .init(row: -1, section: 0)) let didTapSaveButton: PublishSubject = PublishSubject() } @@ -61,11 +62,18 @@ public final class LyricHighlightingViewModel: ViewModelType { .disposed(by: disposeBag) input.didTapHighlighting - .map { $0.item } + .withLatestFrom(input.didTapActivateButton) { ($0, $1) } + .filter { $0.1 } + .map { $0.0.item } .filter { $0 >= 0 } .withLatestFrom(output.dataSource) { ($0, $1) } .filter { index, entities in - guard entities[index].isHighlighting || entities.filter({ $0.isHighlighting }).count < 4 else { + let currentTotalLineCount: Int = entities.filter { $0.isHighlighting } + .map { $0.text.components(separatedBy: "\n").count } + .reduce(0, +) + let nowSelectItemLineCount: Int = entities[index].text + .components(separatedBy: "\n").count + guard entities[index].isHighlighting || (currentTotalLineCount + nowSelectItemLineCount <= 4) else { output.showToast.onNext("가사는 최대 4줄까지 선택 가능합니다.") return false } diff --git a/Projects/Features/MainTabFeature/Sources/ViewControllers/MainTabBarViewController.swift b/Projects/Features/MainTabFeature/Sources/ViewControllers/MainTabBarViewController.swift index e8bff1268..beeff0a99 100644 --- a/Projects/Features/MainTabFeature/Sources/ViewControllers/MainTabBarViewController.swift +++ b/Projects/Features/MainTabFeature/Sources/ViewControllers/MainTabBarViewController.swift @@ -157,7 +157,9 @@ private extension MainTabBarViewController { case "songDetail": let id = params["id"] as? String ?? "" - songDetailPresenter.present(id: id) + let playlistIDs = PlayState.shared.currentPlaylist + .map(\.id) + songDetailPresenter.present(ids: playlistIDs, selectedID: id) default: break diff --git a/Projects/Features/MusicDetailFeature/Sources/MusicDetail/MusicDetailViewController.swift b/Projects/Features/MusicDetailFeature/Sources/MusicDetail/MusicDetailViewController.swift index ca6209cc0..99b3439b8 100644 --- a/Projects/Features/MusicDetailFeature/Sources/MusicDetail/MusicDetailViewController.swift +++ b/Projects/Features/MusicDetailFeature/Sources/MusicDetail/MusicDetailViewController.swift @@ -60,7 +60,7 @@ final class MusicDetailViewController: BaseReactorViewController Void)?, + cancelCompletion: (() -> Void)? + ) -> UIViewController +} + +public extension PlayTypeTogglePopupFactory { + func makeView( + completion: ((_ selectedItemString: String) -> Void)? = nil, + cancelCompletion: (() -> Void)? = nil + ) -> UIViewController { + self.makeView( + completion: completion, + cancelCompletion: cancelCompletion + ) + } +} diff --git a/Projects/Features/MyInfoFeature/Sources/Components/PlayTypeTogglePopupComponent.swift b/Projects/Features/MyInfoFeature/Sources/Components/PlayTypeTogglePopupComponent.swift new file mode 100644 index 000000000..5866f6004 --- /dev/null +++ b/Projects/Features/MyInfoFeature/Sources/Components/PlayTypeTogglePopupComponent.swift @@ -0,0 +1,15 @@ +import MyInfoFeatureInterface +import NeedleFoundation +import UIKit + +public final class PlayTypeTogglePopupComponent: Component, PlayTypeTogglePopupFactory { + public func makeView( + completion: ((_ selectedItemString: String) -> Void)? = nil, + cancelCompletion: (() -> Void)? = nil + ) -> UIViewController { + return PlayTypeTogglePopupViewController( + completion: completion, + cancelCompletion: cancelCompletion + ) + } +} diff --git a/Projects/Features/MyInfoFeature/Sources/Components/SettingComponent.swift b/Projects/Features/MyInfoFeature/Sources/Components/SettingComponent.swift index 85f01520c..a805b41c7 100644 --- a/Projects/Features/MyInfoFeature/Sources/Components/SettingComponent.swift +++ b/Projects/Features/MyInfoFeature/Sources/Components/SettingComponent.swift @@ -17,7 +17,7 @@ public protocol SettingDependency: Dependency { var serviceTermsFactory: any ServiceTermFactory { get } var privacyFactory: any PrivacyFactory { get } var openSourceLicenseFactory: any OpenSourceLicenseFactory { get } - var togglePopUpFactory: any TogglePopUpFactory { get } + var playTypeTogglePopupFactory: any PlayTypeTogglePopupFactory { get } } public final class SettingComponent: Component, SettingFactory { @@ -33,7 +33,7 @@ public final class SettingComponent: Component, SettingFactor serviceTermsFactory: dependency.serviceTermsFactory, privacyFactory: dependency.privacyFactory, openSourceLicenseFactory: dependency.openSourceLicenseFactory, - togglePopUpFactory: dependency.togglePopUpFactory + playTypeTogglePopupFactory: dependency.playTypeTogglePopupFactory ) } } diff --git a/Projects/Features/MyInfoFeature/Sources/Reactors/SettingReactor.swift b/Projects/Features/MyInfoFeature/Sources/Reactors/SettingReactor.swift index db37ff0a0..7679b37c2 100644 --- a/Projects/Features/MyInfoFeature/Sources/Reactors/SettingReactor.swift +++ b/Projects/Features/MyInfoFeature/Sources/Reactors/SettingReactor.swift @@ -37,6 +37,7 @@ final class SettingReactor: Reactor { case updateIsHiddenWithDrawButton(Bool) case changedNotificationAuthorizationStatus(Bool) case updateIsShowActivityIndicator(Bool) + case reloadTableView } enum NavigateType { @@ -59,6 +60,7 @@ final class SettingReactor: Reactor { @Pulse var withDrawButtonDidTap: Bool? @Pulse var confirmRemoveCacheButtonDidTap: Bool? @Pulse var withDrawResult: BaseEntity? + @Pulse var reloadTableView: Void? } var initialState: State @@ -137,6 +139,8 @@ final class SettingReactor: Reactor { newState.notificationAuthorizationStatus = granted case let .updateIsShowActivityIndicator(isShow): newState.isShowActivityIndicator = isShow + case .reloadTableView: + newState.reloadTableView = () } return newState } @@ -161,10 +165,18 @@ final class SettingReactor: Reactor { return .just(.changedNotificationAuthorizationStatus(granted)) } + let updatePlayTypeMutation = PreferenceManager.$playWithYoutubeMusic + .distinctUntilChanged() + .map { $0 ?? .youtube } + .flatMap { playType -> Observable in + return .just(.reloadTableView) + } + return Observable.merge( mutation, updateIsLoggedInMutation, - updatepushNotificationAuthorizationStatusMutation + updatepushNotificationAuthorizationStatusMutation, + updatePlayTypeMutation ) } } diff --git a/Projects/Features/MyInfoFeature/Sources/ViewControllers/PlayTypeTogglePopup/PlayTypeTogglePopupViewController.swift b/Projects/Features/MyInfoFeature/Sources/ViewControllers/PlayTypeTogglePopup/PlayTypeTogglePopupViewController.swift new file mode 100644 index 000000000..7cba2d1db --- /dev/null +++ b/Projects/Features/MyInfoFeature/Sources/ViewControllers/PlayTypeTogglePopup/PlayTypeTogglePopupViewController.swift @@ -0,0 +1,282 @@ +import DesignSystem +import RxCocoa +import RxSwift +import SnapKit +import Then +import UIKit +import Utility + +public final class PlayTypeTogglePopupViewController: UIViewController { + private let dimmView = UIView().then { + $0.backgroundColor = .black.withAlphaComponent(0.4) + } + + private let contentView = UIView().then { + $0.layer.cornerRadius = 24 + $0.backgroundColor = .white + } + + private let titleLabel = WMLabel( + text: "어떻게 재생할까요?", + textColor: DesignSystemAsset.BlueGrayColor.gray900.color, + font: .t2(weight: .bold), + alignment: .center, + lineHeight: UIFont.WMFontSystem.t2().lineHeight, + kernValue: -0.5 + ) + + private let vStackView = UIStackView().then { + $0.axis = .vertical + $0.spacing = 8 + $0.distribution = .fillEqually + } + + private let firstItemButton = PlayTypeTogglePopupItemButtonView() + + private let secondItemButton = PlayTypeTogglePopupItemButtonView() + + private let firstDotImageView = UIImageView().then { + $0.image = DesignSystemAsset.MyInfo.dot.image + } + + private let secondDotImageView = UIImageView().then { + $0.image = DesignSystemAsset.MyInfo.dot.image + } + + private let firstDescriptionLabel = WMLabel( + text: "해당 기능을 사용하려면 앱이 설치되어 있어야 합니다.", + textColor: DesignSystemAsset.BlueGrayColor.gray500.color, + font: .t7(weight: .light), + alignment: .left + ) + + private let secondDescriptionLabel = WMLabel( + text: "일부 노래나 쇼츠는 YouTube Music에서 지원되지 않습니다.", + textColor: DesignSystemAsset.BlueGrayColor.gray500.color, + font: .t7(weight: .light), + alignment: .left + ) + + private let hStackView = UIStackView().then { + $0.axis = .horizontal + $0.spacing = 0 + $0.distribution = .fillEqually + } + + private let cancelButton = UIButton().then { + let cancleButtonBackgroundColor = DesignSystemAsset.BlueGrayColor.blueGray400.color + $0.setBackgroundColor(cancleButtonBackgroundColor, for: .normal) + $0.setTitle("취소", for: .normal) + $0.setTitleColor(DesignSystemAsset.BlueGrayColor.blueGray25.color, for: .normal) + $0.titleLabel?.font = .setFont(.t4(weight: .medium)) + $0.titleLabel?.setTextWithAttributes(alignment: .center) + } + + private let confirmButton = UIButton().then { + let confirmButtonBackgroundColor = DesignSystemAsset.PrimaryColorV2.point.color + $0.setBackgroundColor(confirmButtonBackgroundColor, for: .normal) + $0.setTitle("확인", for: .normal) + $0.setTitleColor(DesignSystemAsset.BlueGrayColor.blueGray25.color, for: .normal) + $0.titleLabel?.font = .setFont(.t4(weight: .medium)) + $0.titleLabel?.setTextWithAttributes(alignment: .center) + } + + private let disposeBag = DisposeBag() + private var selectedItemString: String = "" + private var completion: ((_ selectedItemString: String) -> Void)? + private var cancelCompletion: (() -> Void)? + + init( + completion: ((_ selectedItemString: String) -> Void)? = nil, + cancelCompletion: (() -> Void)? = nil + ) { + self.completion = completion + self.cancelCompletion = cancelCompletion + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + addViews() + setLayout() + configureUI() + setActions() + firstItemButton.setDelegate(self) + secondItemButton.setDelegate(self) + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + contentView.transform = CGAffineTransform(translationX: 0, y: self.view.frame.height) + UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: { + self.contentView.transform = CGAffineTransform.identity + }, completion: nil) + } + + func setActions() { + NotificationCenter.default.rx.notification(UIApplication.didBecomeActiveNotification) + .subscribe(onNext: { [weak self] _ in + self?.secondItemButton.checkAppIsInstalled() + }) + .disposed(by: disposeBag) + + let cancelAction = UIAction { [weak self] _ in + self?.dismiss() + } + + let confirmAction = UIAction { [weak self] _ in + guard let self else { return } + self.completion?(self.selectedItemString) + self.dismiss() + } + cancelButton.addAction(cancelAction, for: .touchUpInside) + confirmButton.addAction(confirmAction, for: .touchUpInside) + } +} + +private extension PlayTypeTogglePopupViewController { + func addViews() { + self.view.addSubviews( + dimmView, + contentView + ) + contentView.addSubviews( + titleLabel, + firstItemButton, + secondItemButton, + firstDotImageView, + firstDescriptionLabel, + secondDotImageView, + secondDescriptionLabel, + hStackView + ) + hStackView.addArrangedSubviews(cancelButton, confirmButton) + } + + func setLayout() { + let is320 = APP_WIDTH() <= 320 + + dimmView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + contentView.snp.makeConstraints { + $0.height.equalTo(is320 ? 364 : 344) + $0.center.equalToSuperview() + $0.horizontalEdges.equalToSuperview().inset(20) + } + + titleLabel.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview().inset(20) + $0.top.equalToSuperview().offset(30) + } + + firstItemButton.snp.makeConstraints { + $0.height.equalTo(60) + $0.top.equalTo(titleLabel.snp.bottom).offset(20) + $0.horizontalEdges.equalToSuperview().inset(20) + } + + secondItemButton.snp.makeConstraints { + $0.height.equalTo(60) + $0.top.equalTo(firstItemButton.snp.bottom).offset(8) + $0.horizontalEdges.equalToSuperview().inset(20) + } + + firstDotImageView.snp.makeConstraints { + $0.size.equalTo(16) + $0.top.equalTo(secondItemButton.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(20) + } + + firstDescriptionLabel.snp.makeConstraints { + $0.top.equalTo(firstDotImageView) + $0.left.equalTo(firstDotImageView.snp.right) + $0.right.equalToSuperview().inset(20) + } + + secondDotImageView.snp.makeConstraints { + $0.size.equalTo(16) + $0.top.equalTo(firstDescriptionLabel.snp.bottom).offset(4) + $0.left.equalToSuperview().offset(20) + } + + secondDescriptionLabel.snp.makeConstraints { + $0.top.equalTo(secondDotImageView) + $0.left.equalTo(secondDotImageView.snp.right) + $0.right.equalToSuperview().inset(20) + } + + hStackView.snp.makeConstraints { + $0.height.equalTo(56) + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + func configureUI() { + self.view.backgroundColor = .clear + contentView.clipsToBounds = true + + let playType = PreferenceManager.playWithYoutubeMusic ?? .youtube + self.selectedItemString = playType.display + + firstItemButton.setTitleWithOption(title: playType.display) + secondItemButton.setTitleWithOption(title: playType.display, shouldCheckAppIsInstalled: true) + + firstItemButton.isSelected = playType == .youtube + secondItemButton.isSelected = playType == .youtubeMusic + + if APP_WIDTH() <= 320 { // 두줄로 표기하기 위함 + firstDescriptionLabel.numberOfLines = 0 + secondDescriptionLabel.numberOfLines = 0 + } + + let gesture = UITapGestureRecognizer(target: self, action: #selector(tappedAround(_:))) + dimmView.addGestureRecognizer(gesture) + dimmView.isUserInteractionEnabled = true + } + + @objc func tappedAround(_ sender: UITapGestureRecognizer) { + dismiss() + } + + func dismiss() { + UIView.animate( + withDuration: 0.3, + delay: 0, + options: .curveEaseInOut, + animations: { [weak self] in + guard let self = self else { return } + let translationY = self.view.frame.height + self.contentView.transform = CGAffineTransform(translationX: 0, y: translationY) + }, + completion: { [weak self] _ in + self?.dismiss(animated: false) + } + ) + } +} + +extension PlayTypeTogglePopupViewController: PlayTypeTogglePopupItemButtonViewDelegate { + func tappedButtonAction(title: String) { + switch title { + case "YouTube": + self.selectedItemString = title + firstItemButton.isSelected = true + secondItemButton.isSelected = false + case "YouTube Music": + self.selectedItemString = title + firstItemButton.isSelected = false + secondItemButton.isSelected = true + default: + break + } + } +} diff --git a/Projects/Features/MyInfoFeature/Sources/ViewControllers/Setting/SettingViewController.swift b/Projects/Features/MyInfoFeature/Sources/ViewControllers/Setting/SettingViewController.swift index b02d66d42..9d9a9c54d 100644 --- a/Projects/Features/MyInfoFeature/Sources/ViewControllers/Setting/SettingViewController.swift +++ b/Projects/Features/MyInfoFeature/Sources/ViewControllers/Setting/SettingViewController.swift @@ -18,7 +18,7 @@ final class SettingViewController: BaseReactorViewController { private var serviceTermsFactory: ServiceTermFactory! private var privacyFactory: PrivacyFactory! private var openSourceLicenseFactory: OpenSourceLicenseFactory! - private var togglePopUpFactory: TogglePopUpFactory! + private var playTypeTogglePopupFactory: PlayTypeTogglePopupFactory! let settingView = SettingView() let settingItemDataSource = SettingItemDataSource() @@ -45,7 +45,7 @@ final class SettingViewController: BaseReactorViewController { serviceTermsFactory: ServiceTermFactory, privacyFactory: PrivacyFactory, openSourceLicenseFactory: OpenSourceLicenseFactory, - togglePopUpFactory: TogglePopUpFactory + playTypeTogglePopupFactory: PlayTypeTogglePopupFactory ) -> SettingViewController { let viewController = SettingViewController(reactor: reactor) viewController.textPopupFactory = textPopupFactory @@ -53,11 +53,18 @@ final class SettingViewController: BaseReactorViewController { viewController.serviceTermsFactory = serviceTermsFactory viewController.privacyFactory = privacyFactory viewController.openSourceLicenseFactory = openSourceLicenseFactory - viewController.togglePopUpFactory = togglePopUpFactory + viewController.playTypeTogglePopupFactory = playTypeTogglePopupFactory return viewController } override func bindState(reactor: SettingReactor) { + reactor.pulse(\.$reloadTableView) + .compactMap { $0 } + .bind(with: self) { owner, _ in + owner.settingView.settingItemTableView.reloadData() + } + .disposed(by: disposeBag) + reactor.state.map(\.isShowActivityIndicator) .distinctUntilChanged() .bind(with: self) { owner, isShow in @@ -222,37 +229,12 @@ extension SettingViewController: UITableViewDelegate { guard let cell = tableView.cellForRow(at: indexPath) as? SettingItemTableViewCell else { return } guard let category = cell.category else { return } - let togglePopUpVC = togglePopUpFactory.makeView( - titleString: "어떻게 재생할까요?", - firstItemString: "YouTube", - secondItemString: "YouTube Music", - descriptionText: "해당 기능을 사용하려면 앱이 설치되어 있어야 합니다.", - completion: {}, - cancelCompletion: {} - ) - togglePopUpVC.modalPresentationStyle = .overFullScreen - - let textPopupVC = textPopupFactory.makeView( - text: "로그아웃 하시겠습니까?", - cancelButtonIsHidden: false, - confirmButtonText: "확인", - cancelButtonText: "취소", - completion: { [weak self] in - guard let self else { return } - let log = SettingAnalyticsLog.completeLogout - LogManager.analytics(log) - - self.reactor?.action.onNext(.confirmLogoutButtonDidTap) - }, - cancelCompletion: {} - ) - switch category { case .appPush: LogManager.analytics(SettingAnalyticsLog.clickNotificationButton) reactor?.action.onNext(.appPushSettingNavigationDidTap) case .playType: - self.present(togglePopUpVC, animated: false) + showPlayTypeTogglePopup() case .serviceTerms: LogManager.analytics(SettingAnalyticsLog.clickTermsOfServiceButton) reactor?.action.onNext(.serviceTermsNavigationDidTap) case .privacy: @@ -266,10 +248,46 @@ extension SettingViewController: UITableViewDelegate { reactor?.action.onNext(.removeCacheButtonDidTap) case .logout: LogManager.analytics(SettingAnalyticsLog.clickLogoutButton) - showBottomSheet(content: textPopupVC, size: .fixed(234)) + showLogoutTextPopup() case .versionInfo: LogManager.analytics(SettingAnalyticsLog.clickVersionButton) reactor?.action.onNext(.versionInfoButtonDidTap) } } + + private func showPlayTypeTogglePopup() { + let togglePopupVC = playTypeTogglePopupFactory.makeView( + completion: { selectedItemString in + switch selectedItemString { + case YoutubePlayType.youtube.display: + PreferenceManager.playWithYoutubeMusic = .youtube + case YoutubePlayType.youtubeMusic.display: + PreferenceManager.playWithYoutubeMusic = .youtubeMusic + default: + break + } + }, + cancelCompletion: {} + ) + togglePopupVC.modalPresentationStyle = .overFullScreen + self.present(togglePopupVC, animated: false) + } + + private func showLogoutTextPopup() { + let textPopUpVC = textPopupFactory.makeView( + text: "로그아웃 하시겠습니까?", + cancelButtonIsHidden: false, + confirmButtonText: "확인", + cancelButtonText: "취소", + completion: { [weak self] in + guard let self else { return } + let log = SettingAnalyticsLog.completeLogout + LogManager.analytics(log) + + self.reactor?.action.onNext(.confirmLogoutButtonDidTap) + }, + cancelCompletion: {} + ) + showBottomSheet(content: textPopUpVC, size: .fixed(234)) + } } diff --git a/Projects/Features/MyInfoFeature/Sources/Views/PlayTypeTogglePopupItemButton.swift b/Projects/Features/MyInfoFeature/Sources/Views/PlayTypeTogglePopupItemButton.swift new file mode 100644 index 000000000..b046957b0 --- /dev/null +++ b/Projects/Features/MyInfoFeature/Sources/Views/PlayTypeTogglePopupItemButton.swift @@ -0,0 +1,172 @@ +import DesignSystem +import SnapKit +import Then +import UIKit + +protocol PlayTypeTogglePopupItemButtonViewDelegate: AnyObject { + func tappedButtonAction(title: String) +} + +final class PlayTypeTogglePopupItemButtonView: UIView { + private let baseView = UIView().then { + $0.layer.cornerRadius = 12 + $0.layer.borderColor = DesignSystemAsset.BlueGrayColor.gray200.color.cgColor + $0.layer.borderWidth = 1 + $0.backgroundColor = .white + } + + private let titleLabel = WMLabel( + text: "", + textColor: DesignSystemAsset.BlueGrayColor.blueGray900.color, + font: .t5(weight: .light), + alignment: .left + ) + + private let imageView = UIImageView().then { + $0.image = DesignSystemAsset.MyInfo.donut.image + $0.contentMode = .scaleAspectFit + } + + private let installButton = UIButton().then { + $0.layer.cornerRadius = 4 + $0.layer.borderWidth = 1 + $0.layer.borderColor = DesignSystemAsset.BlueGrayColor.gray300.color.cgColor + $0.setTitle("미설치", for: .normal) + $0.setBackgroundColor(.white, for: .normal) + $0.setBackgroundColor(.lightGray, for: .highlighted) + $0.setTitleColor(DesignSystemAsset.BlueGrayColor.gray400.color, for: .normal) + $0.setTitleColor(.white, for: .highlighted) + $0.titleLabel?.font = UIFont.WMFontSystem.t7(weight: .bold).font + $0.clipsToBounds = true + $0.isHidden = true + } + + private let button = UIButton() + + private weak var delegate: PlayTypeTogglePopupItemButtonViewDelegate? + + private var shouldCheckAppIsInstalled: Bool = false + + var isSelected: Bool = false { + didSet { + didChangedSelection(isSelected) + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .white + addViews() + setLayout() + setActions() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setDelegate(_ delegate: PlayTypeTogglePopupItemButtonViewDelegate) { + self.delegate = delegate + } + + func setTitleWithOption( + title: String, + shouldCheckAppIsInstalled: Bool = false + ) { + self.titleLabel.text = title + self.shouldCheckAppIsInstalled = shouldCheckAppIsInstalled + if shouldCheckAppIsInstalled { + checkAppIsInstalled() + } + } + + @discardableResult + func checkAppIsInstalled() -> Bool { + let isInstalled: Bool + if let url = URL(string: "youtubemusic://"), UIApplication.shared.canOpenURL(url) { + isInstalled = true + } else { + isInstalled = false + } + installButton.isHidden = isInstalled + button.isEnabled = isInstalled + + return isInstalled + } +} + +private extension PlayTypeTogglePopupItemButtonView { + func didChangedSelection(_ isSelected: Bool) { + UIView.animate(withDuration: 0.2) { [weak self] in + guard let self else { return } + + self.baseView.layer.borderColor = isSelected ? + DesignSystemAsset.PrimaryColorV2.point.color.cgColor : + DesignSystemAsset.BlueGrayColor.blueGray200.color.cgColor + + self.baseView.layer.borderWidth = isSelected ? 2 : 1 + } + + self.imageView.image = isSelected ? + DesignSystemAsset.MyInfo.donutColor.image : + DesignSystemAsset.MyInfo.donut.image + + let font = isSelected ? + UIFont.WMFontSystem.t5(weight: .medium) : + UIFont.WMFontSystem.t5(weight: .light) + self.titleLabel.setFont(font) + } + + func setActions() { + let buttonAction = UIAction { [weak self] _ in + guard let self = self else { return } + self.delegate?.tappedButtonAction(title: titleLabel.text ?? "") + } + button.addAction(buttonAction, for: .touchUpInside) + + installButton.addAction { + let youtubeMusicAppStoreURL = "itms-apps://apps.apple.com/app/id1017492454" + if let url = URL(string: youtubeMusicAppStoreURL) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + } + + func addViews() { + addSubview(baseView) + addSubview(titleLabel) + addSubview(imageView) + addSubview(button) + addSubview(installButton) + } + + func setLayout() { + baseView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + titleLabel.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.equalToSuperview().inset(16) + $0.trailing.equalTo(imageView.snp.leading).offset(-20) + } + + imageView.snp.makeConstraints { + $0.size.equalTo(24) + $0.trailing.equalToSuperview().inset(20) + $0.centerY.equalToSuperview() + } + + button.snp.makeConstraints { + $0.edges.equalTo(baseView) + } + + installButton.snp.makeConstraints { + $0.width.equalTo(55) + $0.height.equalTo(24) + $0.trailing.equalToSuperview().inset(20) + $0.centerY.equalToSuperview() + } + } +} diff --git a/Projects/Features/MyInfoFeature/Sources/Views/SettingItemTableViewCell.swift b/Projects/Features/MyInfoFeature/Sources/Views/SettingItemTableViewCell.swift index 9d1fad27b..600d4b9ca 100644 --- a/Projects/Features/MyInfoFeature/Sources/Views/SettingItemTableViewCell.swift +++ b/Projects/Features/MyInfoFeature/Sources/Views/SettingItemTableViewCell.swift @@ -72,12 +72,12 @@ private extension SettingItemTableViewCell { switch type { case let .navigate(category): let pushNotificationAuthorizationStatus = PreferenceManager.pushNotificationAuthorizationStatus ?? false - let playWithYoutubeMusic = PreferenceManager.playWithYoutubeMusic ?? false + let playType = PreferenceManager.playWithYoutubeMusic ?? .youtube switch category { case .appPush: self.subTitleLabel.text = pushNotificationAuthorizationStatus ? "켜짐" : "꺼짐" case .playType: - self.subTitleLabel.text = playWithYoutubeMusic ? "YouTube Music" : "YouTube" + self.subTitleLabel.text = playType.display default: self.subTitleLabel.text = "" } diff --git a/Projects/Features/MyInfoFeature/Testing/SettingComponentStub.swift b/Projects/Features/MyInfoFeature/Testing/SettingComponentStub.swift index f5d1b47eb..ea6cd31d8 100644 --- a/Projects/Features/MyInfoFeature/Testing/SettingComponentStub.swift +++ b/Projects/Features/MyInfoFeature/Testing/SettingComponentStub.swift @@ -23,7 +23,20 @@ public final class SettingComponentStub: SettingFactory { signInFactory: SignInComponentStub(), serviceTermsFactory: ServiceTermComponentStub(), privacyFactory: PrivacyComponentStub(), - openSourceLicenseFactory: OpenSourceLicenseComponentStub() + openSourceLicenseFactory: OpenSourceLicenseComponentStub(), + playTypeTogglePopupFactory: PlayTypeTogglePopupComponentStub() + ) + } +} + +final class PlayTypeTogglePopupComponentStub: PlayTypeTogglePopupFactory { + public func makeView( + completion: ((_ selectedItemString: String) -> Void)? = nil, + cancelCompletion: (() -> Void)? = nil + ) -> UIViewController { + return PlayTypeTogglePopupViewController( + completion: completion, + cancelCompletion: cancelCompletion ) } } diff --git a/Projects/Features/PlaylistFeature/Interface/PlaylistDetailNavigator.swift b/Projects/Features/PlaylistFeature/Interface/PlaylistDetailNavigator.swift index 098e9506d..6e50ad01e 100644 --- a/Projects/Features/PlaylistFeature/Interface/PlaylistDetailNavigator.swift +++ b/Projects/Features/PlaylistFeature/Interface/PlaylistDetailNavigator.swift @@ -4,13 +4,13 @@ import UIKit public protocol PlaylistDetailNavigator { var playlistDetailFactory: any PlaylistDetailFactory { get } - func navigateWmPlaylistDetail(key: String) + func navigateWMPlaylistDetail(key: String) func navigatePlaylistDetail(key: String) } public extension PlaylistDetailNavigator where Self: UIViewController { - func navigateWmPlaylistDetail(key: String) { + func navigateWMPlaylistDetail(key: String) { let dest = playlistDetailFactory.makeWmView(key: key) self.navigationController?.pushViewController(dest, animated: true) diff --git a/Projects/Features/PlaylistFeature/Sources/Components/WakmusicPlaylistDetailComponent.swift b/Projects/Features/PlaylistFeature/Sources/Components/WakmusicPlaylistDetailComponent.swift index a7d6643de..f2ccd621b 100644 --- a/Projects/Features/PlaylistFeature/Sources/Components/WakmusicPlaylistDetailComponent.swift +++ b/Projects/Features/PlaylistFeature/Sources/Components/WakmusicPlaylistDetailComponent.swift @@ -8,7 +8,7 @@ import SignInFeatureInterface import UIKit public protocol WakmusicPlaylistDetailDependency: Dependency { - var fetchWmPlaylistDetailUseCase: any FetchWmPlaylistDetailUseCase { get } + var fetchWMPlaylistDetailUseCase: any FetchWMPlaylistDetailUseCase { get } var containSongsFactory: any ContainSongsFactory { get } var textPopupFactory: any TextPopupFactory { get } var songDetailPresenter: any SongDetailPresentable { get } @@ -21,7 +21,7 @@ public final class WakmusicPlaylistDetailComponent: Component Observable { return .concat([ .just(.updateLoadingState(true)), - fetchWmPlaylistDetailUseCase.execute(id: key) + fetchWMPlaylistDetailUseCase.execute(id: key) .asObservable() .flatMap { data -> Observable in return .concat([ diff --git a/Projects/Features/PlaylistFeature/Sources/ViewControllers/MyPlaylistDetailViewController.swift b/Projects/Features/PlaylistFeature/Sources/ViewControllers/MyPlaylistDetailViewController.swift index 4260b0980..da51d79fe 100644 --- a/Projects/Features/PlaylistFeature/Sources/ViewControllers/MyPlaylistDetailViewController.swift +++ b/Projects/Features/PlaylistFeature/Sources/ViewControllers/MyPlaylistDetailViewController.swift @@ -53,6 +53,7 @@ final class MyPlaylistDetailViewController: BaseReactorViewController UICollectionViewLayoutAttributes { setNeedsLayout() layoutIfNeeded() + let collectionViewWidth = superview?.bounds.width ?? UIScreen.main.bounds.width + + let maxWidth = collectionViewWidth * 0.6 + let size = contentView.systemLayoutSizeFitting(layoutAttributes.size) var newFrame = layoutAttributes.frame - newFrame.size = size + newFrame.size = .init( + width: min(maxWidth, size.width), + height: size.height + ) layoutAttributes.frame = newFrame return layoutAttributes } diff --git a/Projects/Features/StorageFeature/Sources/ViewControllers/LikeStorageViewController.swift b/Projects/Features/StorageFeature/Sources/ViewControllers/LikeStorageViewController.swift index 0054e8b1e..a65b5b347 100644 --- a/Projects/Features/StorageFeature/Sources/ViewControllers/LikeStorageViewController.swift +++ b/Projects/Features/StorageFeature/Sources/ViewControllers/LikeStorageViewController.swift @@ -216,7 +216,9 @@ final class LikeStorageViewController: BaseReactorViewController