diff --git a/Halmap.xcodeproj/project.pbxproj b/Halmap.xcodeproj/project.pbxproj index 2b31e5b..08f9a01 100644 --- a/Halmap.xcodeproj/project.pbxproj +++ b/Halmap.xcodeproj/project.pbxproj @@ -15,7 +15,6 @@ 87F6ECF1291677AC004533C4 /* Halmap+UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F6ECF0291677AC004533C4 /* Halmap+UIApplication.swift */; }; 87F6ECF52916B331004533C4 /* Halmap+UINavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F6ECF42916B331004533C4 /* Halmap+UINavigationController.swift */; }; 87F6ECF72916BB44004533C4 /* Halmap+UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F6ECF62916BB44004533C4 /* Halmap+UIView.swift */; }; - A813C77429C779C400CF73FE /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A813C77329C779C400CF73FE /* GoogleService-Info.plist */; }; A81FFC7A28F15D9900B0FC7C /* HalmapApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81FFC7928F15D9900B0FC7C /* HalmapApp.swift */; }; A81FFC7C28F15D9900B0FC7C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81FFC7B28F15D9900B0FC7C /* ContentView.swift */; }; A81FFC7E28F15D9C00B0FC7C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A81FFC7D28F15D9C00B0FC7C /* Assets.xcassets */; }; @@ -30,6 +29,9 @@ A81FFC9F28F180B200B0FC7C /* SongContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81FFC9E28F180B200B0FC7C /* SongContentView.swift */; }; A81FFCA528F1B84000B0FC7C /* Halmap+Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81FFCA428F1B84000B0FC7C /* Halmap+Font.swift */; }; A81FFCA728F1B84A00B0FC7C /* Halmap+Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81FFCA628F1B84A00B0FC7C /* Halmap+Color.swift */; }; + A852F0282A46EDDA00078B47 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A852F0272A46EDDA00078B47 /* GoogleService-Info.plist */; }; + A8A8260D2A4CE2D400F470DF /* PlayListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A8260C2A4CE2D400F470DF /* PlayListRow.swift */; }; + A8A8260F2A4CE37200F470DF /* PlayListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A8260E2A4CE37200F470DF /* PlayListView.swift */; }; A8D1E92229169DAD00BF242C /* test-song.m4a in Resources */ = {isa = PBXBuildFile; fileRef = A8D1E92129169DAD00BF242C /* test-song.m4a */; }; A8F8BA9929C8CE5B00FCB229 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8F8BA9829C8CE5B00FCB229 /* AudioManager.swift */; }; B2699E0D28F1CB7E00267A4F /* Pretendard-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = B2699E0C28F1CB7E00267A4F /* Pretendard-Bold.otf */; }; @@ -43,6 +45,7 @@ F939EBBC29D5920F005ED8CA /* OffsetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F939EBBB29D5920F005ED8CA /* OffsetModifier.swift */; }; F949284F28F2AFB800901F27 /* Music.json in Resources */ = {isa = PBXBuildFile; fileRef = F949284E28F2AFB800901F27 /* Music.json */; }; F95CE98B2917A8EF00FFE213 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = F95CE98A2917A8EF00FFE213 /* Settings.bundle */; }; + F95EF67C2A40695C00C6C3E7 /* HalfModalPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F95EF67B2A40695C00C6C3E7 /* HalfModalPresentationController.swift */; }; F98F622929B4C9BD0025F50E /* Themes.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98F622829B4C9BD0025F50E /* Themes.swift */; }; F98F622B29B4CB450025F50E /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98F622A29B4CB450025F50E /* ThemeManager.swift */; }; F98F623129B59CE60025F50E /* TeamName.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98F623029B59CE60025F50E /* TeamName.swift */; }; @@ -63,7 +66,6 @@ 87F6ECF0291677AC004533C4 /* Halmap+UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Halmap+UIApplication.swift"; sourceTree = ""; }; 87F6ECF42916B331004533C4 /* Halmap+UINavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Halmap+UINavigationController.swift"; sourceTree = ""; }; 87F6ECF62916BB44004533C4 /* Halmap+UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Halmap+UIView.swift"; sourceTree = ""; }; - A813C77329C779C400CF73FE /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; A81FFC7628F15D9900B0FC7C /* Halmap.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Halmap.app; sourceTree = BUILT_PRODUCTS_DIR; }; A81FFC7928F15D9900B0FC7C /* HalmapApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HalmapApp.swift; sourceTree = ""; }; A81FFC7B28F15D9900B0FC7C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -79,6 +81,9 @@ A81FFC9E28F180B200B0FC7C /* SongContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SongContentView.swift; sourceTree = ""; }; A81FFCA428F1B84000B0FC7C /* Halmap+Font.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Halmap+Font.swift"; sourceTree = ""; }; A81FFCA628F1B84A00B0FC7C /* Halmap+Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Halmap+Color.swift"; sourceTree = ""; }; + A852F0272A46EDDA00078B47 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + A8A8260C2A4CE2D400F470DF /* PlayListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayListRow.swift; sourceTree = ""; }; + A8A8260E2A4CE37200F470DF /* PlayListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayListView.swift; sourceTree = ""; }; A8D1E92129169DAD00BF242C /* test-song.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "test-song.m4a"; sourceTree = ""; }; A8F8BA9829C8CE5B00FCB229 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; }; B2699E0B28F1CB5100267A4F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -93,6 +98,7 @@ F939EBBB29D5920F005ED8CA /* OffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffsetModifier.swift; sourceTree = ""; }; F949284E28F2AFB800901F27 /* Music.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Music.json; sourceTree = ""; }; F95CE98A2917A8EF00FFE213 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; + F95EF67B2A40695C00C6C3E7 /* HalfModalPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HalfModalPresentationController.swift; sourceTree = ""; }; F98F622829B4C9BD0025F50E /* Themes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Themes.swift; sourceTree = ""; }; F98F622A29B4CB450025F50E /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; F98F623029B59CE60025F50E /* TeamName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamName.swift; sourceTree = ""; }; @@ -126,7 +132,7 @@ A81FFC6D28F15D9900B0FC7C = { isa = PBXGroup; children = ( - A813C77329C779C400CF73FE /* GoogleService-Info.plist */, + A852F0272A46EDDA00078B47 /* GoogleService-Info.plist */, B2699E0E28F1CB8800267A4F /* Font */, A81FFC7828F15D9900B0FC7C /* Halmap */, A81FFC7728F15D9900B0FC7C /* Products */, @@ -145,6 +151,7 @@ A81FFC7828F15D9900B0FC7C /* Halmap */ = { isa = PBXGroup; children = ( + A852F01A2A462F9300078B47 /* Utills */, A81FFC8E28F1770500B0FC7C /* View */, B2699E0B28F1CB5100267A4F /* Info.plist */, F98F622729B4C9A80025F50E /* Theme */, @@ -203,8 +210,9 @@ A81FFC9B28F1808700B0FC7C /* SongInformation */ = { isa = PBXGroup; children = ( + A8A826102A4CE5D900F470DF /* Lyric */, + A8A8260B2A4CE2C300F470DF /* PlayList */, A81FFC9C28F180A000B0FC7C /* SongPlayerView.swift */, - A81FFC9E28F180B200B0FC7C /* SongContentView.swift */, F9B3B7B629B842DD00232BB8 /* SongDetailView.swift */, ); path = SongInformation; @@ -222,6 +230,31 @@ path = Extensions; sourceTree = ""; }; + A852F01A2A462F9300078B47 /* Utills */ = { + isa = PBXGroup; + children = ( + F95EF67B2A40695C00C6C3E7 /* HalfModalPresentationController.swift */, + ); + path = Utills; + sourceTree = ""; + }; + A8A8260B2A4CE2C300F470DF /* PlayList */ = { + isa = PBXGroup; + children = ( + A8A8260E2A4CE37200F470DF /* PlayListView.swift */, + A8A8260C2A4CE2D400F470DF /* PlayListRow.swift */, + ); + path = PlayList; + sourceTree = ""; + }; + A8A826102A4CE5D900F470DF /* Lyric */ = { + isa = PBXGroup; + children = ( + A81FFC9E28F180B200B0FC7C /* SongContentView.swift */, + ); + path = Lyric; + sourceTree = ""; + }; B2699E0E28F1CB8800267A4F /* Font */ = { isa = PBXGroup; children = ( @@ -342,11 +375,11 @@ files = ( F95CE98B2917A8EF00FFE213 /* Settings.bundle in Resources */, F9A2DAA62A07DE9A008B84A9 /* Preview Assets.xcassets in Resources */, - A813C77429C779C400CF73FE /* GoogleService-Info.plist in Resources */, A81FFC8128F15D9C00B0FC7C /* Preview Assets.xcassets in Resources */, B2699E0D28F1CB7E00267A4F /* Pretendard-Bold.otf in Resources */, A81FFC7E28F15D9C00B0FC7C /* Assets.xcassets in Resources */, B2699E1028F1CB9700267A4F /* Pretendard-Medium.otf in Resources */, + A852F0282A46EDDA00078B47 /* GoogleService-Info.plist in Resources */, F949284F28F2AFB800901F27 /* Music.json in Resources */, A8D1E92229169DAD00BF242C /* test-song.m4a in Resources */, ); @@ -368,10 +401,13 @@ F939EBBC29D5920F005ED8CA /* OffsetModifier.swift in Sources */, F9A178222A1BF5CB003E8E4C /* HalfSheetView.swift in Sources */, 87F6ECF52916B331004533C4 /* Halmap+UINavigationController.swift in Sources */, + A8A8260D2A4CE2D400F470DF /* PlayListRow.swift in Sources */, + A8A8260F2A4CE37200F470DF /* PlayListView.swift in Sources */, A81FFCA528F1B84000B0FC7C /* Halmap+Font.swift in Sources */, F98F8D5D29D85B0C00F9C956 /* RequestSongView.swift in Sources */, A81FFC7C28F15D9900B0FC7C /* ContentView.swift in Sources */, A81FFC9D28F180A000B0FC7C /* SongPlayerView.swift in Sources */, + F95EF67C2A40695C00C6C3E7 /* HalfModalPresentationController.swift in Sources */, A81FFC8628F15D9C00B0FC7C /* Halmap.xcdatamodeld in Sources */, 87F6ECF72916BB44004533C4 /* Halmap+UIView.swift in Sources */, F91BE5FB29B18AF800F7E488 /* MainTabView.swift in Sources */, @@ -524,7 +560,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_ASSET_PATHS = "\"Halmap/Preview Content\""; - DEVELOPMENT_TEAM = Y6P5ZFWBM8; + DEVELOPMENT_TEAM = 22A6RK2943; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Halmap/Info.plist; @@ -559,7 +595,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_ASSET_PATHS = "\"Halmap/Preview Content\""; - DEVELOPMENT_TEAM = Y6P5ZFWBM8; + DEVELOPMENT_TEAM = 22A6RK2943; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Halmap/Info.plist; diff --git a/Halmap/Assets.xcassets/Image/2023/NC23.imageset/Contents.json b/Halmap/Assets.xcassets/Image/2023/NC23.imageset/Contents.json deleted file mode 100644 index 287fb9e..0000000 --- a/Halmap/Assets.xcassets/Image/2023/NC23.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "NC23.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Halmap/Assets.xcassets/Image/2023/NC23.imageset/NC23.png b/Halmap/Assets.xcassets/Image/2023/NC23.imageset/NC23.png deleted file mode 100644 index 43f3442..0000000 Binary files a/Halmap/Assets.xcassets/Image/2023/NC23.imageset/NC23.png and /dev/null differ diff --git a/Halmap/Assets.xcassets/Image/Player/NCPlayer.imageset/Contents.json b/Halmap/Assets.xcassets/Image/Player/NCPlayer.imageset/Contents.json deleted file mode 100644 index 9c8ba38..0000000 --- a/Halmap/Assets.xcassets/Image/Player/NCPlayer.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "NCPlayer.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Halmap/Assets.xcassets/Image/Player/NCPlayer.imageset/NCPlayer.png b/Halmap/Assets.xcassets/Image/Player/NCPlayer.imageset/NCPlayer.png deleted file mode 100644 index f3e0975..0000000 Binary files a/Halmap/Assets.xcassets/Image/Player/NCPlayer.imageset/NCPlayer.png and /dev/null differ diff --git "a/Halmap/Assets.xcassets/\355\214\200 \354\225\250\353\262\224 \354\273\244\353\262\204/NCAlbum.imageset/Contents.json" "b/Halmap/Assets.xcassets/\355\214\200 \354\225\250\353\262\224 \354\273\244\353\262\204/NCAlbum.imageset/Contents.json" index a0647a8..afd4eba 100644 --- "a/Halmap/Assets.xcassets/\355\214\200 \354\225\250\353\262\224 \354\273\244\353\262\204/NCAlbum.imageset/Contents.json" +++ "b/Halmap/Assets.xcassets/\355\214\200 \354\225\250\353\262\224 \354\273\244\353\262\204/NCAlbum.imageset/Contents.json" @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "NcAlbum.png", + "filename" : "NCAlbum.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/Halmap/Data/DataManager.swift b/Halmap/Data/DataManager.swift index 96467da..0679ab9 100644 --- a/Halmap/Data/DataManager.swift +++ b/Halmap/Data/DataManager.swift @@ -20,6 +20,8 @@ class DataManager: ObservableObject { @Published var playerSongs: [Song] = [] @Published var teamSongs: [Song] = [] @Published var favoriteSongs = PersistenceController.shared.fetchFavoriteSong() + // 추가 + @Published var PlayListSongs = PersistenceController.shared.fetchPlayListSong() @Published var playerSongsAll = [[Song]](repeating: [], count: 10) @Published var teamSongsAll = [[Song]](repeating: [], count: 10) diff --git a/Halmap/Halmap.xcdatamodeld/Halmap.xcdatamodel/contents b/Halmap/Halmap.xcdatamodeld/Halmap.xcdatamodel/contents index 8017e45..72239a9 100644 --- a/Halmap/Halmap.xcdatamodeld/Halmap.xcdatamodel/contents +++ b/Halmap/Halmap.xcdatamodeld/Halmap.xcdatamodel/contents @@ -1,10 +1,11 @@ - + + diff --git a/Halmap/Persistence.swift b/Halmap/Persistence.swift index 06e31bf..ca223b0 100644 --- a/Halmap/Persistence.swift +++ b/Halmap/Persistence.swift @@ -10,7 +10,6 @@ import SwiftUI struct PersistenceController { @AppStorage("selectedTeam") var selectedTeam = "Hanwha" - @FetchRequest(entity: CollectedSong.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \CollectedSong.date, ascending: true)], animation: .default) private var collectedSongs: FetchedResults static let shared = PersistenceController() @@ -29,9 +28,20 @@ struct PersistenceController { container.viewContext.automaticallyMergesChangesFromParent = true } - func saveSongs(song: SongInfo, playListTitle: String?) { + func saveSongs(song: SongInfo, playListTitle: String?, menuType: MenuType, collectedSongs: FetchedResults) { let context = container.viewContext let collectedSong = CollectedSong(context: context) + + switch menuType { + case .cancelLiked: + break + case .playNext: + // TODO: 현재 곡 다음순서로 넣는 로직 필요 + collectedSong.order = Int64(collectedSongs.count) + case .playLast: + collectedSong.order = Int64(collectedSongs.count) + } + collectedSong.id = song.id collectedSong.title = song.title collectedSong.info = song.info @@ -41,6 +51,7 @@ struct PersistenceController { collectedSong.playListTitle = playListTitle collectedSong.team = song.team collectedSong.date = Date() + if context.hasChanges { do { @@ -52,6 +63,39 @@ struct PersistenceController { } } + func createCollectedSong(song: SongInfo, playListTitle: String?) -> CollectedSong { + let context = container.viewContext + let collectedSong = CollectedSong(context: context) + collectedSong.id = song.id + collectedSong.title = song.title + collectedSong.info = song.info + collectedSong.lyrics = song.lyrics + collectedSong.url = song.url + collectedSong.type = song.type + collectedSong.playListTitle = playListTitle + collectedSong.team = song.team + collectedSong.date = Date() + + print("collectedSong", collectedSong, song.title) + return collectedSong + } + + /// CollectedSong을 생성하기 위해 BufferList에 넣은 곡을 지우는 함수. + func resetBufferList(song: CollectedSong){ + + if song.playListTitle == "bufferPlayList" { + container.viewContext.delete(song) + } + + do { + try container.viewContext.save() + } catch { + container.viewContext.rollback() + print(error.localizedDescription) + } + } + + /// PlayList에서 곡을 지우는 함수. func deleteSongs(song: CollectedSong) { container.viewContext.delete(song) @@ -64,6 +108,70 @@ struct PersistenceController { } } + /// index를 이용하여 PlayListd에서 곡을 지우는 함수. + func deleteSong(at indexs: IndexSet, from results: FetchedResults) { + + for index in indexs { + let song = results[index] + container.viewContext.delete(song) + } + + do { + try container.viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + + /// defaultPlayList 순서를 변경하는 함수 + func moveDefaultPlayListSong(from source: IndexSet, to destination: Int, based results: FetchedResults){ + + guard let itemToMove = source.first else { return } + + if itemToMove < destination{ + var startIndex = itemToMove + 1 + let endIndex = destination - 1 + var startOrder = results[itemToMove].order + while startIndex <= endIndex{ + results[startIndex].order = startOrder + startOrder = startOrder + 1 + startIndex = startIndex + 1 + } + results[itemToMove].order = startOrder + } + + else if destination < itemToMove{ + var startIndex = destination + let endIndex = itemToMove - 1 + var startOrder = results[destination].order + 1 + let newOrder = results[destination].order + while startIndex <= endIndex{ + results[startIndex].order = startOrder + startOrder = startOrder + 1 + startIndex = startIndex + 1 + } + results[itemToMove].order = newOrder + } + + do{ + try container.viewContext.save() + } + catch{ + print(error.localizedDescription) + } + } + + /// defaultPlayList 앞에 추가하는 함수 + func appendDefaultPlayListSong(song: SongInfo){ + + } + /// defaultPlayList 뒤에 추가하는 함수 + func pushBackDefaultPlayListSong(song: SongInfo){ + + } + + func fetchFavoriteSong() -> [CollectedSong] { let fetchRequest: NSFetchRequest = CollectedSong.fetchRequest() @@ -74,7 +182,26 @@ struct PersistenceController { } } - func findFavoriteSong(song: Song) -> CollectedSong { + func findFavoriteSong(song: Song, collectedSongs: FetchedResults) -> CollectedSong { + if let index = collectedSongs.firstIndex(where: {song.id == $0.id}) { + return collectedSongs[index] + } else { + return CollectedSong() + } + } + + func fetchPlayListSong() -> [CollectedSong] { + let fetchRequest: NSFetchRequest = CollectedSong.fetchRequest() + + do{ + return try container.viewContext.fetch(fetchRequest) + }catch { + return [] + } + } + + + func findPlayListSong(song: Song, collectedSongs: FetchedResults) -> CollectedSong { if let index = collectedSongs.firstIndex(where: {song.id == $0.id}) { return collectedSongs[index] } else { diff --git a/Halmap/Utills/HalfModalPresentationController.swift b/Halmap/Utills/HalfModalPresentationController.swift new file mode 100644 index 0000000..ae80719 --- /dev/null +++ b/Halmap/Utills/HalfModalPresentationController.swift @@ -0,0 +1,36 @@ +// +// HalfModalPresentationController.swift +// Halmap +// +// Created by JeonJimin on 2023/06/19. +// + +import SwiftUI + +class HalfSheetController: UIHostingController where Content : View { + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if let presentation = sheetPresentationController { + presentation.detents = [.medium()] + presentation.prefersGrabberVisible = true + presentation.largestUndimmedDetentIdentifier = .none + } + } +} + +struct HalfSheet: UIViewControllerRepresentable where Content : View { + + private let content: Content + + @inlinable init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + func makeUIViewController(context: Context) -> HalfSheetController { + return HalfSheetController(rootView: content) + } + + func updateUIViewController(_: HalfSheetController, context: Context) { + } +} diff --git a/Halmap/View/HalfSheetView.swift b/Halmap/View/HalfSheetView.swift index 557138c..dd21802 100644 --- a/Halmap/View/HalfSheetView.swift +++ b/Halmap/View/HalfSheetView.swift @@ -10,40 +10,41 @@ import SwiftUI struct HalfSheetView: View { @Environment(\.managedObjectContext) var managedObjectContext let persistence = PersistenceController.shared - @ObservedObject var collectedSong: CollectedSong - @State var songData: Song - @State var song: CollectedSong = CollectedSong() - var team: String @Binding var showSheet: Bool - + @Binding var collectedSongData: CollectedSong? var body: some View { VStack(spacing: 0) { - HStack(spacing: 16) { - Image("\(team)SongListImage") - .frame(width: 52, height: 52) - .shadow(color: .black.opacity(0.25), radius: 4, x: 0, y: 4) - VStack(alignment: .leading, spacing: 6) { - Text(songData.title ) - .font(Font.Halmap.CustomBodyBold) - .foregroundColor(.customBlack) - Text(TeamName(rawValue: team )?.fetchTeamNameKr() ?? ".") - .font(Font.Halmap.CustomCaptionMedium) - .foregroundColor(.customDarkGray) + // Capsule() + // .fill(Color.gray) + // .frame(width: 35, height: 5) + // .padding(.vertical, 18) + if let collectedSongData { + HStack(spacing: 16) { + Image(collectedSongData.type ? "\(collectedSongData.team ?? "")Album" : "\(collectedSongData.team ?? "")Player") + .resizable() + .frame(width: 52, height: 52) + .cornerRadius(10) + .shadow(color: .black.opacity(0.25), radius: 4, x: 0, y: 4) + VStack(alignment: .leading, spacing: 6) { + Text(collectedSongData.title ?? "") + .font(Font.Halmap.CustomBodyBold) + .foregroundColor(.customBlack) + Text(TeamName(rawValue: collectedSongData.team ?? "" )?.fetchTeamNameKr() ?? ".") + .font(Font.Halmap.CustomCaptionMedium) + .foregroundColor(.customDarkGray) + } + .frame(maxWidth: .infinity, alignment: .leading) } - .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 20) + .padding(.top, 27) + Divider() + .overlay(Color.customGray.opacity(0.6)) + .padding(EdgeInsets(top: 20, leading: 0, bottom: 29, trailing: 0)) + MenuItem(menuType: .cancelLiked, collectedSong: collectedSongData, showSheet: $showSheet) + MenuItem(menuType: .playNext, collectedSong: collectedSongData, showSheet: $showSheet) + MenuItem(menuType: .playLast, collectedSong: collectedSongData, showSheet: $showSheet) + Spacer() } - .padding(.horizontal, 20) - .padding(.top, 27) - Divider() - .overlay(Color.customGray.opacity(0.6)) - .padding(EdgeInsets(top: 20, leading: 0, bottom: 29, trailing: 0)) - MenuItem(menuType: .cancelLiked, collectedSong: collectedSong, showSheet: $showSheet) - MenuItem(menuType: .playNext, collectedSong: collectedSong, showSheet: $showSheet) - MenuItem(menuType: .playLast, collectedSong: collectedSong, showSheet: $showSheet) - Spacer() - } - .onAppear() { - print("****\(songData)") } } } @@ -53,9 +54,12 @@ struct MenuItem: View { @ObservedObject var collectedSong: CollectedSong @Binding var showSheet: Bool + let persistence = PersistenceController.shared + @FetchRequest(entity: CollectedSong.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \CollectedSong.order, ascending: true)], predicate: PlayListFilter(filter: "defaultPlayList").predicate, animation: .default) private var defaultPlayListSongs: FetchedResults + var body: some View { Button { - menuType.fetchFunction(collectedSong: collectedSong) + menuType.fetchFunction(collectedSong: collectedSong, collectedSongs: defaultPlayListSongs) showSheet.toggle() } label: { HStack(spacing: 24) { @@ -98,17 +102,26 @@ enum MenuType { } } - func fetchFunction(collectedSong: CollectedSong) { + func fetchFunction(collectedSong: CollectedSong, collectedSongs: FetchedResults) { let persistence = PersistenceController.shared switch self { case .cancelLiked: return persistence.deleteSongs(song: collectedSong) case .playNext: //TODO: 바로 다음에 재생 기능 추가 - return (print("play next")) + let song = collectedSong + let songInfo = SongInfo(id: song.id ?? "", team: song.team ?? "", type: song.type, title: song.title ?? "", lyrics: song.lyrics ?? "", info: song.info ?? "", url: song.url ?? "") + persistence.resetBufferList(song: collectedSong) + + return persistence.saveSongs(song: songInfo, playListTitle: "defaultPlayList", menuType: .playNext, collectedSongs: collectedSongs) + case .playLast: //TODO: 맨 마지막에 재생 기능 추가 - return (print("play last")) + let song = collectedSong + let songInfo = SongInfo(id: song.id ?? "", team: song.team ?? "", type: song.type, title: song.title ?? "", lyrics: song.lyrics ?? "", info: song.info ?? "", url: song.url ?? "") + persistence.resetBufferList(song: collectedSong) + + return persistence.saveSongs(song: songInfo, playListTitle: "defaultPlayList", menuType: .playLast, collectedSongs: collectedSongs) } } } diff --git a/Halmap/View/MainSongListTabView.swift b/Halmap/View/MainSongListTabView.swift index 4b8325b..0d7173b 100644 --- a/Halmap/View/MainSongListTabView.swift +++ b/Halmap/View/MainSongListTabView.swift @@ -10,13 +10,24 @@ import SwiftUI struct MainSongListTabView: View { @AppStorage("selectedTeam") var selectedTeam = "Hanwha" + // @EnvironmentObject var dataManager: DataManager + @Environment(\.managedObjectContext) private var viewContext +// @FetchRequest(entity: CollectedSong.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \CollectedSong.date, ascending: true)], predicate: PlayListFilter(filter: "playList").predicate, animation: .default) private var collectedSongs: FetchedResults @State private var showingTeamChaingView: Bool = false @State var index = 0 // SongInformationView - @State private var showingFullScreenCover = false + @State private var isShowingFullScreenCover = false + // HalfSheetModalView + @State private var isShowingSheet = false + @State private var isActivateNavigationLink = false + @Environment(\.dismiss) private var sheetDismiss + @Environment(\.dismiss) private var navigationLinkDismiss + + let persistence = PersistenceController.shared + @State var collectedSong: CollectedSong? init() { Color.setColor(selectedTeam) @@ -43,8 +54,11 @@ struct MainSongListTabView: View { .padding(.horizontal, 20) TabView(selection: $index) { + + // MARK: 팀 응원가 탭 List { ForEach(dataManager.teamSongs) { song in + let music = Song(id: song.id, type: song.type, title: song.title, @@ -59,7 +73,8 @@ struct MainSongListTabView: View { info: song.info, url: song.url) - NavigationLink(destination: SongDetailView(song: music, team: selectedTeam)) { + ZStack() { + // Stack 1: Contents HStack(spacing: 16) { Image(dataManager.checkSeasonSong(data: songInfo) ? "\(selectedTeam)23" : "\(selectedTeam)Album") .resizable() @@ -76,12 +91,42 @@ struct MainSongListTabView: View { } .frame(maxWidth: .infinity, alignment: .leading) .lineLimit(1) + }.background( NavigationLink("", destination: SongDetailView(song: music, team: selectedTeam), isActive: $isActivateNavigationLink ).opacity(0) ) + .onTapGesture { + isActivateNavigationLink = true + isShowingSheet = false + navigationLinkDismiss() + } + + // Stack 2: Button + HStack { + Spacer() + VStack(alignment: .trailing){ + Button { + print("버튼") + isShowingSheet = true + isActivateNavigationLink = false + sheetDismiss() + + collectedSong = persistence.createCollectedSong(song: songInfo, playListTitle: "bufferPlayList") + + + } label: { + Image(systemName: "ellipsis").foregroundColor(.customDarkGray) + } + .sheet(isPresented: $isShowingSheet) { + HalfSheet{ + HalfSheetView(showSheet: $isShowingSheet, collectedSongData: $collectedSong) + } + } + }.frame(width: 20, height: 20) } } } .listRowInsets(EdgeInsets(top: 15, leading: 0, bottom: 15, trailing: 0)) .listRowBackground(Color.systemBackground) .listRowSeparatorTint(Color.customGray) + RequestSongView(buttonColor: Color.HalmacPoint) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) .listRowBackground(Color.systemBackground) @@ -91,6 +136,8 @@ struct MainSongListTabView: View { .listStyle(.plain) .tag(0) + + // MARK: 선수 응원가 탭 List { ForEach(dataManager.playerSongs) { song in let music = Song(id: song.id, @@ -107,9 +154,10 @@ struct MainSongListTabView: View { lyrics: song.lyrics, info: song.info, url: song.url) + - - NavigationLink(destination: SongDetailView(song: music, team: selectedTeam)) { + ZStack{ + // Stack 1: Contents HStack(spacing: 16) { Image(dataManager.checkSeasonSong(data: songInfo) ? "\(selectedTeam)23" : "\(selectedTeam)Player") .resizable() @@ -126,6 +174,37 @@ struct MainSongListTabView: View { } .frame(maxWidth: .infinity, alignment: .leading) .lineLimit(1) + }.background( NavigationLink("", destination: SongDetailView(song: music, team: selectedTeam), isActive: $isActivateNavigationLink ).opacity(0) ) + .onTapGesture { + isActivateNavigationLink = true + isShowingSheet = false + navigationLinkDismiss() + } + + + + + // Stack 2: Button + HStack { + Spacer() + VStack(alignment: .trailing){ + Button { + print("버튼") + isShowingSheet = true + isActivateNavigationLink = false + sheetDismiss() + + collectedSong = persistence.createCollectedSong(song: songInfo, playListTitle: "bufferPlayList") + + } label: { + Image(systemName: "ellipsis").foregroundColor(.customDarkGray) + } + .sheet(isPresented: $isShowingSheet) { + HalfSheet{ + HalfSheetView(showSheet: $isShowingSheet, collectedSongData: $collectedSong) + } + } + }.frame(width: 20, height: 20) } } } diff --git a/Halmap/View/ScalingHeaderView.swift b/Halmap/View/ScalingHeaderView.swift index e7ba822..534f400 100644 --- a/Halmap/View/ScalingHeaderView.swift +++ b/Halmap/View/ScalingHeaderView.swift @@ -9,12 +9,16 @@ import SwiftUI struct ScalingHeaderView: View { @EnvironmentObject var dataManager: DataManager - let maxHeight: CGFloat = 216 + let maxHeight: CGFloat = UIScreen.getHeight(216) var topEdge: CGFloat @State var offset: CGFloat = 0 + @State var isShowSheet = false + @State var collectedSongData: CollectedSong? + @State var isDraged = false @FetchRequest(entity: CollectedSong.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \CollectedSong.date, ascending: true)], predicate: PlayListFilter(filter: "favorite").predicate, animation: .default) private var collectedSongs: FetchedResults + let persistence = PersistenceController.shared var body: some View { @@ -22,28 +26,43 @@ struct ScalingHeaderView: View { VStack(spacing: 15) { GeometryReader { proxy in VStack(spacing: 0) { - TopBar(topEdge: topEdge, offset: $offset) + topBar .frame(maxWidth: .infinity) .frame(height: getHeaderHeight(), alignment: .bottom) .overlay( topTitle - .opacity(topTitleOpacity()) + .opacity(checkScrollRequirement(listCount: collectedSongs.count) && isDraged ? 1 : 0) ) HStack() { Text("총 \(collectedSongs.count)곡") .font(Font.Halmap.CustomCaptionBold) .foregroundColor(.customDarkGray) Spacer() + Button { + withAnimation { + isDraged.toggle() + } + } label: { + HStack(spacing: 5) { + Image(systemName: "play.circle.fill") + .foregroundColor(.mainGreen) + .font(.system(size: 20)) + Text("전체 재생하기") + .font(Font.Halmap.CustomCaptionBold) + .foregroundColor(.mainGreen) + } + .opacity(checkScrollRequirement(listCount: collectedSongs.count) && isDraged ? 1 : 0) + } } .padding(.horizontal, 20) - .padding(.vertical, 17) + .padding(.vertical, UIScreen.getHeight(17)) Divider() .overlay(Color.customGray.opacity(0.6)) .padding(.horizontal, 20) } .background(Color.systemBackground) } - .frame(height: maxHeight + 48) + .frame(height: maxHeight + UIScreen.getHeight(48)) .offset(y: -offset) .zIndex(1) @@ -55,8 +74,8 @@ struct ScalingHeaderView: View { .frame(width: 200, height: 200) .padding(.top, 60) } else { - ForEach(collectedSongs) { favoriteSong in - let song = Song( + ForEach(self.collectedSongs, id: \.self) { favoriteSong in + var song = Song( id: favoriteSong.id ?? "", type: favoriteSong.type , title: favoriteSong.title ?? "" , @@ -95,10 +114,21 @@ struct ScalingHeaderView: View { .lineLimit(1) Button { - persistence.deleteSongs(song: favoriteSong) + print("scaling favorite",favoriteSong) + self.collectedSongData = favoriteSong + + song = Song(id: favoriteSong.id ?? "", type: favoriteSong.type, title: favoriteSong.title ?? "", lyrics: favoriteSong.lyrics ?? "", info: favoriteSong.info ?? "", url: favoriteSong.url ?? "") + + isShowSheet.toggle() + } label: { - Image(systemName: "heart.fill") - .foregroundColor(.mainGreen) + Image(systemName: "ellipsis") + .foregroundColor(.black.opacity(0.2)) + } + .sheet(isPresented: $isShowSheet) { + HalfSheet { + HalfSheetView(showSheet: $isShowSheet, collectedSongData: $collectedSongData) + } } } .padding(.horizontal, 20) @@ -111,10 +141,48 @@ struct ScalingHeaderView: View { } } .modifier(OffsetModifier(offset: $offset)) + .onChange(of: offset) { _ in + if offset > -UIScreen.getHeight(90) { + withAnimation { + isDraged = false + } + } else if offset < 0 { + withAnimation { + isDraged = true + } + } + } } .coordinateSpace(name: "StorageScroll") .background(Color.systemBackground) + } + + var topBar: some View { + ZStack(alignment: .bottom) { + Image("storageTop") + .resizable() + HStack { + Text("보관함") + .font(Font.Halmap.CustomLargeTitle) + Spacer() + Button { + withAnimation { + isDraged.toggle() + } + } label: { + Image(systemName: "play.circle.fill") + .foregroundColor(.mainGreen) + .font(.system(size: 50)) + } + } + .padding(EdgeInsets(top: 0, leading: 20, bottom: 20, trailing: 20)) + } + .opacity(checkScrollRequirement(listCount: collectedSongs.count) && isDraged ? 0 : 1) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + .background(Color.systemBackground) + } + var topTitle: some View { VStack{ HStack { @@ -129,45 +197,11 @@ struct ScalingHeaderView: View { } func getHeaderHeight() -> CGFloat { - let topHeight = maxHeight + offset - - return topHeight >= (59 + topEdge) ? topHeight : (59 + topEdge) - } - func topTitleOpacity() -> CGFloat { - let progress = -offset*2 / (maxHeight - (59 + topEdge)) - return progress - } - func getOpacity() -> CGFloat { - let progress = -offset*2 / 40 - let opacity = 1 - progress - return offset < 0 ? opacity : 1 + isDraged ? (59 + topEdge) : maxHeight + offset } -} - -struct TopBar: View { - - let topEdge: CGFloat - @Binding var offset: CGFloat - var body: some View { - ZStack(alignment: .bottom) { - Image("storageTop") - .resizable() - HStack { - Text("보관함") - .font(Font.Halmap.CustomLargeTitle) - Spacer() - } - .padding(EdgeInsets(top: 0, leading: 20, bottom: 20, trailing: 20)) - } - .opacity(getOpacity()) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) - .background(Color.systemBackground) - } - func getOpacity() -> CGFloat { - let progress = -offset / 40 - let opacity = 1 - progress - return offset < 0 ? opacity : 1 + func checkScrollRequirement(listCount: Int) -> Bool{ + UIScreen.screenHeight - (59 + topEdge) - CGFloat(75 * listCount) <= 0 ? true : false } } diff --git a/Halmap/View/SongInformation/SongContentView.swift b/Halmap/View/SongInformation/Lyric/SongContentView.swift similarity index 100% rename from Halmap/View/SongInformation/SongContentView.swift rename to Halmap/View/SongInformation/Lyric/SongContentView.swift diff --git a/Halmap/View/SongInformation/PlayList/PlayListRow.swift b/Halmap/View/SongInformation/PlayList/PlayListRow.swift new file mode 100644 index 0000000..0d09c39 --- /dev/null +++ b/Halmap/View/SongInformation/PlayList/PlayListRow.swift @@ -0,0 +1,37 @@ +// +// PlayListRow.swift +// Halmap +// +// Created by Yeni Hwang on 2023/06/29. +// + +import SwiftUI + +struct PlayListRow: View { + @EnvironmentObject var dataManager: DataManager + @State var songInfo: SongInfo + + var body: some View { + HStack{ + Image(dataManager.checkSeasonSong(data: songInfo) ? "\(songInfo.team ?? "")23" : "\( songInfo.team ?? "NC")\(songInfo.type ? "Player" : "Album")") + .resizable() + .frame(width: 40, height: 40) + .cornerRadius(8) + VStack(alignment: .leading, spacing: 8) { + Text(songInfo.title ?? "test ") + .font(Font.Halmap.CustomBodyMedium) + .foregroundColor(.white) + Text(TeamName(rawValue: songInfo.team ?? "NC")?.fetchTeamNameKr() ?? ".") + .font(Font.Halmap.CustomCaptionMedium) + .foregroundColor(.customDarkGray) + }.padding(.horizontal, 16) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + Image(systemName: "text.justify") + .frame(width: 18, height: 18) + .foregroundStyle(.gray) + } + .padding(.horizontal, 20) // 40 -> 20 값 조정 + .frame(height: 50) // 70 -> 50값 조정 + } +} diff --git a/Halmap/View/SongInformation/PlayList/PlayListView.swift b/Halmap/View/SongInformation/PlayList/PlayListView.swift new file mode 100644 index 0000000..39d1ea3 --- /dev/null +++ b/Halmap/View/SongInformation/PlayList/PlayListView.swift @@ -0,0 +1,95 @@ +// +// PlayListView.swift +// Halmap +// +// Created by Yeni Hwang on 2023/06/24. +// + +import SwiftUI + +struct PlayListView: View { + + + @EnvironmentObject var audioManager: AudioManager + @AppStorage("selectedTeam") var selectedTeam = "Hanwha" + @EnvironmentObject var dataManager: DataManager + @Binding var song: Song + + let persistence = PersistenceController.shared + @FetchRequest(entity: CollectedSong.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \CollectedSong.order, ascending: true)], predicate: PlayListFilter(filter: "defaultPlayList").predicate, animation: .default) private var collectedSongs: FetchedResults + + @FetchRequest(entity: CollectedSong.entity(), + sortDescriptors: [ + NSSortDescriptor(keyPath: \CollectedSong.id, ascending: true) + ], animation: .default + ) private var playListSongs: FetchedResults + + var body: some View { + + ZStack{ + if collectedSongs.count != 0 { + List { + ForEach(collectedSongs, id: \.self) { playListSong in + + let song = Song( + id: playListSong.id ?? "", + type: playListSong.type , + title: playListSong.title ?? "" , + lyrics: playListSong.lyrics ?? "", + info: playListSong.info ?? "", + url: playListSong.url ?? "" + ) + + let songInfo = SongInfo( + id: playListSong.id ?? "", + team: playListSong.team ?? "", + type: playListSong.type , + title: playListSong.title ?? "" , + lyrics: playListSong.lyrics ?? "", + info: playListSong.info ?? "", + url: playListSong.url ?? "" + ) + + PlayListRow(songInfo: songInfo) + .onTapGesture { + self.song = song + audioManager.removePlayer() + audioManager.AMset(song: song, selectedTeam: selectedTeam) + } + + + }.onDelete { indexSet in + persistence.deleteSong(at: indexSet, from: collectedSongs) + }.onMove { indexSet, destination in + persistence.moveDefaultPlayListSong(from: indexSet, to: destination, based: collectedSongs) + } + .listRowBackground(Color.clear) + } + .listStyle(.plain) + .modifier(ListBackgroundModifier()) + } + + }.background(Color("\(selectedTeam)Sub")) + } + + func findPlayListSong(at offsets: IndexSet) -> CollectedSong { + if let index = playListSongs.firstIndex(where: {song.id == $0.id}) { + return playListSongs[index] + } else { + return CollectedSong() + } + } +} + +struct ListBackgroundModifier: ViewModifier { + + @ViewBuilder + func body(content: Content) -> some View { + if #available(iOS 16.0, *) { + content + .scrollContentBackground(.hidden) + } else { + content + } + } +} diff --git a/Halmap/View/SongInformation/SongDetailView.swift b/Halmap/View/SongInformation/SongDetailView.swift index 4b822a8..3a50a42 100644 --- a/Halmap/View/SongInformation/SongDetailView.swift +++ b/Halmap/View/SongInformation/SongDetailView.swift @@ -16,6 +16,7 @@ struct SongDetailView: View { @State var song: Song @State var team: String + @State var isPlayListView = false @State var isScrolled = false @State var isFavorite = false @@ -30,7 +31,14 @@ struct SongDetailView: View { Color("\(team)Sub") .ignoresSafeArea() - SongContentView(song: $song, team: $team, isScrolled: $isScrolled) + if isPlayListView { + VStack{ + Rectangle().foregroundColor(.clear).frame(height: 10) + PlayListView(song: $song) + } + } else { + SongContentView(song: $song, team: $team, isScrolled: $isScrolled) + } VStack(spacing: 0) { Rectangle() @@ -43,10 +51,28 @@ struct SongDetailView: View { .foregroundColor(Color(UIColor.clear)) } Spacer() - Rectangle() - .frame(height: 120) - .background(Color.fetchBottomGradient(color: Color("\(team)Sub"))) - .foregroundColor(Color(UIColor.clear)) + + ZStack{ + Rectangle() + .frame(height: 120) + .background(Color.fetchBottomGradient(color: Color("\(team)Sub"))) + .foregroundColor(Color(UIColor.clear)) + + // PlayListButton + HStack(){ + Spacer() + Button(action: { + isPlayListView.toggle() + }, label: { + ZStack { + Circle().foregroundColor(Color("\(team)Background")).frame(width: 43, height: 43) + Image(systemName: isPlayListView ? "quote.bubble.fill" : "list.bullet").foregroundColor(.white) + + } + }) + }.padding(20) + } + ZStack(alignment: .center) { Rectangle() .frame(height: UIScreen.getHeight(120)) @@ -65,7 +91,7 @@ struct SongDetailView: View { persistence.deleteSongs(song: findFavoriteSong()) } else { let songInfo = SongInfo(id: song.id, team: team, type: song.type, title: song.title, lyrics: song.lyrics, info: song.info, url: song.url) - persistence.saveSongs(song: songInfo, playListTitle: "favorite") + persistence.saveSongs(song: songInfo, playListTitle: "favorite", menuType: .cancelLiked, collectedSongs: favoriteSongs) } isFavorite.toggle() } label: {