From 1a433575526e700dbd0fb955e3f0450841e6358f Mon Sep 17 00:00:00 2001 From: "Mr.zhou" <1422157428@qq.com> Date: Mon, 13 May 2024 13:55:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E9=83=A8=E5=88=86bug=E7=9A=84?= =?UTF-8?q?=E5=A4=84=E7=90=86=E4=BB=A5=E5=8F=8A=E6=90=9C=E7=B4=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E7=9A=84=E5=88=9D=E6=AD=A5=E6=90=AD=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MusicPlayer.xcodeproj/project.pbxproj | 24 + .../xcschemes/MusicPlayer.xcscheme | 7 + .../Frame@2x.png | Bin 558 -> 345 bytes .../Frame@3x.png | Bin 993 -> 469 bytes .../Contents.json | 22 + .../Group_1597880487@2x.png | Bin 0 -> 405 bytes .../Group_1597880487@3x.png | Bin 0 -> 638 bytes .../Player_Single'logo.imageset/Contents.json | 22 + .../Group_1597880488@2x.png | Bin 0 -> 524 bytes .../Group_1597880488@3x.png | Bin 0 -> 796 bytes .../Common/Extension(扩展)/Notification.swift | 8 + .../Common/Macro(宏定义与全局量)/Macro.swift | 12 +- .../Tool(工具封装)/MP_NetWorkManager.swift | 283 ++++++-- .../Tool(工具封装)/MP_PlayerManager.swift | 259 ++++++-- .../MPPositive_JsonSearchResults.swift | 622 ++++++++++++++++++ .../MPPositive_JsonSearchSuggestions.swift | 190 ++++++ ...MPPositive_SearchSuggestionItemModel.swift | 17 + .../MPPositive_SongViewModel.swift | 60 +- .../MPPositive_PlayerLoadViewModel.swift | 80 ++- .../MPPositive_TabBarController.swift | 55 +- .../MPPositive_PlayerListShowViewController.swift | 23 +- .../MPPositive_PlayerViewController.swift | 138 ++-- ...MPPositive_SearchResultShowViewController.swift | 18 + .../MPPositive_SearchViewController.swift | 208 +++++- .../Base/MPPositive_BottomShowView.swift | 64 +- ...Positive_PlayerListShowTableViewCell.swift | 8 + .../Player/MPPositive_PlayerLyricView.swift | 4 +- ...ve_SearchSuggestionItemTableViewCell.swift | 52 ++ ...ve_SearchSuggestionListTableViewCell.swift | 65 ++ 29 files changed, 1973 insertions(+), 268 deletions(-) create mode 100644 MusicPlayer/Assets.xcassets/Positive/Player/Player_Shuffle'logo.imageset/Contents.json create mode 100644 MusicPlayer/Assets.xcassets/Positive/Player/Player_Shuffle'logo.imageset/Group_1597880487@2x.png create mode 100644 MusicPlayer/Assets.xcassets/Positive/Player/Player_Shuffle'logo.imageset/Group_1597880487@3x.png create mode 100644 MusicPlayer/Assets.xcassets/Positive/Player/Player_Single'logo.imageset/Contents.json create mode 100644 MusicPlayer/Assets.xcassets/Positive/Player/Player_Single'logo.imageset/Group_1597880488@2x.png create mode 100644 MusicPlayer/Assets.xcassets/Positive/Player/Player_Single'logo.imageset/Group_1597880488@3x.png create mode 100644 MusicPlayer/MP/MPPositive/Models/JsonStructs/MPPositive_JsonSearchResults.swift create mode 100644 MusicPlayer/MP/MPPositive/Models/JsonStructs/MPPositive_JsonSearchSuggestions.swift create mode 100644 MusicPlayer/MP/MPPositive/Models/Models/MPPositive_SearchSuggestionItemModel.swift create mode 100644 MusicPlayer/MP/MPPositive/ViewControllers/Search(搜索页)/MPPositive_SearchResultShowViewController.swift create mode 100644 MusicPlayer/MP/MPPositive/Views/Search/MPPositive_SearchSuggestionItemTableViewCell.swift create mode 100644 MusicPlayer/MP/MPPositive/Views/Search/MPPositive_SearchSuggestionListTableViewCell.swift diff --git a/MusicPlayer.xcodeproj/project.pbxproj b/MusicPlayer.xcodeproj/project.pbxproj index 852660e..50087f6 100644 --- a/MusicPlayer.xcodeproj/project.pbxproj +++ b/MusicPlayer.xcodeproj/project.pbxproj @@ -150,6 +150,12 @@ CBEE8E342BEB16BB007DA798 /* MPPositive_PlayerSilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEE8E332BEB16BB007DA798 /* MPPositive_PlayerSilder.swift */; }; CBEE8E362BEB2604007DA798 /* MPPositive_PlayerLyricView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEE8E352BEB2604007DA798 /* MPPositive_PlayerLyricView.swift */; }; CBEE8E382BEB92CC007DA798 /* MPPositive_SongViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEE8E372BEB92CC007DA798 /* MPPositive_SongViewModel.swift */; }; + CBFECE352BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */; }; + CBFECE372BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE362BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift */; }; + CBFECE392BF0CFFA00E07DC4 /* MPPositive_SearchSuggestionItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */; }; + CBFECE3B2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE3A2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift */; }; + CBFECE3D2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE3C2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift */; }; + CBFECE3F2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE3E2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -299,6 +305,12 @@ CBEE8E332BEB16BB007DA798 /* MPPositive_PlayerSilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PlayerSilder.swift; sourceTree = ""; }; CBEE8E352BEB2604007DA798 /* MPPositive_PlayerLyricView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PlayerLyricView.swift; sourceTree = ""; }; CBEE8E372BEB92CC007DA798 /* MPPositive_SongViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SongViewModel.swift; sourceTree = ""; }; + CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionItemTableViewCell.swift; sourceTree = ""; }; + CBFECE362BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonSearchSuggestions.swift; sourceTree = ""; }; + CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionItemModel.swift; sourceTree = ""; }; + CBFECE3A2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionListTableViewCell.swift; sourceTree = ""; }; + CBFECE3C2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultShowViewController.swift; sourceTree = ""; }; + CBFECE3E2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonSearchResults.swift; sourceTree = ""; }; E2C6C85BFD4CD80DBA96D149 /* Pods-MusicPlayer.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MusicPlayer.release.xcconfig"; path = "Target Support Files/Pods-MusicPlayer/Pods-MusicPlayer.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -646,6 +658,7 @@ CBE1CB4B2BDE440E00701D57 /* MPPositive_ListHeaderModel.swift */, CBB5D31E2BDF711600CC333D /* MPPositive_SongItemModel.swift */, CBB75B0A2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift */, + CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */, ); path = Models; sourceTree = ""; @@ -757,6 +770,7 @@ isa = PBXGroup; children = ( CB0918A02BD26B0A006D2B39 /* MPPositive_SearchViewController.swift */, + CBFECE3C2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift */, ); path = "Search(搜索页)"; sourceTree = ""; @@ -771,6 +785,8 @@ CBCB50212BD118BB009760B3 /* Search */ = { isa = PBXGroup; children = ( + CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */, + CBFECE3A2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift */, ); path = Search; sourceTree = ""; @@ -809,6 +825,8 @@ CBDD516C2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift */, CB5661282BE09D0500CFD014 /* MPPositive_JsonPlayer.swift */, CBB9F9DC2BEDCFEE008338DE /* MPPositive_JsonLyrics.swift */, + CBFECE362BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift */, + CBFECE3E2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift */, ); path = JsonStructs; sourceTree = ""; @@ -1045,12 +1063,14 @@ CBE1CB4C2BDE440E00701D57 /* MPPositive_ListHeaderModel.swift in Sources */, CBEE8E362BEB2604007DA798 /* MPPositive_PlayerLyricView.swift in Sources */, CBCB4FF22BD11402009760B3 /* MPSideA_AboutViewController.swift in Sources */, + CBFECE372BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift in Sources */, CB0918A52BD26E16006D2B39 /* MPPositive_BottomShowView.swift in Sources */, CBB5D31F2BDF711600CC333D /* MPPositive_SongItemModel.swift in Sources */, CBCAFB5A2BB3C2A000BC6520 /* LayoutConstraint.swift in Sources */, CBCB4FEF2BD11402009760B3 /* MPSideA_NavigationController.swift in Sources */, CBCB35212BD7ACE900802900 /* MPPositive_JsonBrowse.swift in Sources */, CBCB4FF62BD11402009760B3 /* MPSideA_DeleteViewController.swift in Sources */, + CBFECE392BF0CFFA00E07DC4 /* MPPositive_SearchSuggestionItemModel.swift in Sources */, CBDD516F2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift in Sources */, CBEE8E382BEB92CC007DA798 /* MPPositive_SongViewModel.swift in Sources */, CBCB50122BD11402009760B3 /* MPSideA_Home_FirstListCollectionViewCell.swift in Sources */, @@ -1070,6 +1090,7 @@ CBCB4FEA2BD11402009760B3 /* MPSideA_MusicModel.swift in Sources */, CBBFA91A2BBA846600057FD5 /* CoreDataDelegete.swift in Sources */, CB56612D2BE0DF8C00CFD014 /* MP_WebWork.swift in Sources */, + CBFECE352BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift in Sources */, CBCB50042BD11402009760B3 /* MPSideA_HomeViewController.swift in Sources */, CB09189D2BD25F63006D2B39 /* MPPositive_CustomTabBarItem.swift in Sources */, CBCB4FFC2BD11402009760B3 /* MPSideA_RenameViewController.swift in Sources */, @@ -1108,6 +1129,7 @@ CBD313612BD6453A0015D227 /* MPPositive_HomeListFifthCollectionViewCell.swift in Sources */, CBE1CB4A2BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift in Sources */, CBCB4FF82BD11402009760B3 /* MPSideA_MoreViewController.swift in Sources */, + CBFECE3B2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift in Sources */, CBCB50142BD11402009760B3 /* MPSideA_Home_FourthListCollectionViewCell.swift in Sources */, CB5661292BE09D0500CFD014 /* MPPositive_JsonPlayer.swift in Sources */, CBCB50062BD11402009760B3 /* MPSideA_PlayerViewController.swift in Sources */, @@ -1123,6 +1145,7 @@ 009662372BB14A5A00FCA65F /* MusicPlayer.xcdatamodeld in Sources */, CB09189B2BD25F50006D2B39 /* MPPositive_CustomTabBarView.swift in Sources */, CBE1CB502BDE4CC500701D57 /* MPPositive_ListHeaderViewModel.swift in Sources */, + CBFECE3D2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift in Sources */, CBCAFB5F2BB3C55500BC6520 /* DateTime.swift in Sources */, CBD958D22BB6600500666B0D /* MP_PlayerSlider.swift in Sources */, CBCC23532BEE596E004D7A57 /* MPPositive_PlayerListShowTableViewCell.swift in Sources */, @@ -1140,6 +1163,7 @@ CBCB321A2BD7578500802900 /* MP_LocationManager.swift in Sources */, CBCB4FEB2BD11402009760B3 /* MPSideA_MusicViewModel.swift in Sources */, CBCB4FF02BD11402009760B3 /* MPSideA_PresentationController.swift in Sources */, + CBFECE3F2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift in Sources */, CBC6874B2BC2B0710023ECA6 /* String.swift in Sources */, CBD3135F2BD642D90015D227 /* MPPositive_HomeListFourthCollectionViewCell.swift in Sources */, ); diff --git a/MusicPlayer.xcodeproj/xcshareddata/xcschemes/MusicPlayer.xcscheme b/MusicPlayer.xcodeproj/xcshareddata/xcschemes/MusicPlayer.xcscheme index 36878cd..fc613e4 100644 --- a/MusicPlayer.xcodeproj/xcshareddata/xcschemes/MusicPlayer.xcscheme +++ b/MusicPlayer.xcodeproj/xcshareddata/xcschemes/MusicPlayer.xcscheme @@ -49,6 +49,13 @@ ReferencedContainer = "container:MusicPlayer.xcodeproj"> + + + + lFzsbIW%C3@Wv1rD}^Rnw;3vuXdU zUpD7nlB}a#dX3UU?>gsz9}|tO#h>QQv9{4ldZu9b%RjdKinG?va|L|w)qxc#Y*OX-ZIBGPR~*#e$VMFkoT01Q_ehCtzsq9b(-^op%CjYg&j!q8v=n{beU5w1Tpg>kCnM{C9%1lw^>bP0l+XkK2x+W) delta 544 zcmV+*0^j}F0srgz6G33i++;yp?8Cl`G2+#0mp!!l6LYrzX5HZ z1fFMuJCa_NY|xWq`zml3I00+{?<9R)t+^KWVJ*6tRYlD1=DQ3Y)WRKOkmk2A{x+CBq31~v+h%rbz5b}nymDjU~;2SA*;+L))H z?XdAtg`LYNn1AdH#X`A%-Of@a$0<0#&Up$BvU5D(U^~YFPO@_hpzXuJQ{Y1BN$<$3 zza+LVCBTk-i>EdL-bmd?@h%;SO@PraJ_Ew)*-;M|M7FO04^#I$6S563K}M@|C-rl) z)NBV_$!7aBa3457LCSu>4D@p9TH;MHD#igO+r#R35r5tubcx0SW@;W!w#Q}ai8+87 zXuPeX#gB8^5_18Q?a>p)BPnitJj!cgK47M1OIcbDu(2#H4_H~2_7AYIEWMWWBXaYf zg(wj(vXi_mX|v{J*XG*Ztt@?(^met@-`{UHfpcl_y`(R-o3`Yqx6MW18}Kpzf?NNv iwkVp~+D9+_yOsCF=|Pi3BgH3w*F5z3QS{cO z3y!pHi3#xuNIv!YWR$Q*po*tG+APX4xxWh=OQ>#X zyj#JTUi|c#w!|{-h+mhF&tRLPl3yAj`*ykYoARvQ=N~UEerB`Ixh`pQxfV5?00DDSM?wIu&K&6g00WyzL_t(&L+zT!i&Q}n zhrcrCgjr0Ob41Mfq&E*9{6iHyC?4F!iyjOI6$C*P4|-A*L%Bbxh2H1v?Ci`7{oHLB z7?^o8{q?W9tGcR;DK>11@iGnkzbihi@RU>7-Dd+^fE5Y+JAdGVnf=UlLEK%|Iumd+ z`!(J?$C$Lc&j5}BXMkDBL*IeBX7(~S-~e3?Yy*}7zk#=A_Nvodi@;L%3b3!kmxs)3 zI746wbOcxeNH7fv^a6NLRC_YO!)7*=1F*Zxbk_psfDPl!=^Jp{%-*D*w+`6d1-d(4 z3%dy5z#~AGseku>M{9uHU1y5S_b{-iIQMbDABS0MU$+A|yvAza0B&@#P4q$hm0~`YmOu~FStw-L#u}@cp`L6GApLP>IPv8h1bo%a%mlQ5z(LBw5CBBJNgVB23ef?fFpP~8T+)?*mn@PW&)4crwSTQ17!pcgDgUI3Rf6B z%D^fyZfR(kYS z1OBzwg?}g=SpM|}px`3MQ97{lz$buHNlrK?@N$7g=I&DY8n6Z^>*8M4qLmLE&;nJg zte9C>zg$l2YJdY;kPD(rqXeAZH$AEYPRSy@s)F36hRwVl;80wZg6$EMsQ!~yBkNMW zKHyOzcb93alzF}Ze_cOt0bK4&g|L3A2>n<^bAP~L<|=b;j)P5I235PUW`Ps6m8sN$ zcDB%3(T|GVp2-6^0hhvpRw@2aP zhS8&eKVr}@qv{r8h%b6RWKmXf_Kb(I>{X|6kXH?^OQ#PIjb z?8oTyoq#XL2|4D*2Rt-0HF{+;++CLvrI_T-inlSfAKo{!=jntyHJ2WWmcBFq_zc`J zv-jE1#zZ2;BokN1k4mv>Li-3jRljAi15fy$R7@n-w*pGRj*i#Br?iDHC-A6i$!9A@ zmx+G?zIK{#F1e@L)^8fPwxw?9kERuFXyI%7+bO5;A2b;lTN6FD7ytkO07*qoM6N<$ Ef*$74nE(I) diff --git a/MusicPlayer/Assets.xcassets/Positive/Player/Player_Shuffle'logo.imageset/Contents.json b/MusicPlayer/Assets.xcassets/Positive/Player/Player_Shuffle'logo.imageset/Contents.json new file mode 100644 index 0000000..ab8b539 --- /dev/null +++ b/MusicPlayer/Assets.xcassets/Positive/Player/Player_Shuffle'logo.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_1597880487@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_1597880487@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MusicPlayer/Assets.xcassets/Positive/Player/Player_Shuffle'logo.imageset/Group_1597880487@2x.png b/MusicPlayer/Assets.xcassets/Positive/Player/Player_Shuffle'logo.imageset/Group_1597880487@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..404165d62479bca76506c8186c6cd21fcf0dd7f8 GIT binary patch literal 405 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDB3?!H8JlO)I)B}7%T-^(N($dmWQ&S-%kO2hg z>FGc&8~{aDF7xRGYT+#j@(X5&N&Y{n!BZ^%)xp+#QE?Gx7#J7@JY5_^Dj3gBw)bLo z6gj5uC=Yh;kI=bur4_pGRuy>~D3kmp}F> zP2$o@eRJ#bo}YJHc74mQF?sv{|368mtyQ-kMz2iQN#B36xo^XoFM27dPwgJO6_|d7 zJ-HugO=}GweBe0{$TiFs_C%bP0l+XkKEUB*| literal 0 HcmV?d00001 diff --git a/MusicPlayer/Assets.xcassets/Positive/Player/Player_Shuffle'logo.imageset/Group_1597880487@3x.png b/MusicPlayer/Assets.xcassets/Positive/Player/Player_Shuffle'logo.imageset/Group_1597880487@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..bcc050ad51352b0e2dca45664bb040357293deee GIT binary patch literal 638 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~aez;Vt9yY@T3T8v7yub5DJeiE zkO60>r>DbZAyNy2j?V>Z7bpqx3uZ8zxKqdZ{?#u6%cRf5vsJT1r@dleV0`82;uuoF z_%`(Rq%{gWPlS{fH2nX+)tzTo&!fmRkE{E9>~pSKT_`c<6pXL=xP3yDH{ZO<$m^ZW zoNsm+O3Jyu?&9%bGZt>QHeM@U9^@~&#YJ$_jdZ({lDepP|ezEJmx=d^uV*$?~2mV}qF7%kqmH&E2_>ihgJvlhQ~ zZukT)MI%A&v-6WyK{YXhjVwbZElUM(v z84GxSpA8h`@lslp=;-u@bzjft3Fb!~M2rFx6pUteqzV|No^@9-7Cdb8lzaBuUpu)b zt#*BMam(SRQ^rO=*5&fwHjHJLH=EJ7T&#Up%j=KNrQBnfh5nSLtoC;+<0<)=(&3Z$ z`%(9zw|sSpMXu*0eG;SgTK=8+cS(Br#YWFr8EfYIGq&D*CDL%>%)Pd))+?8DbsR6* zy0Ghd;G>;u1Ts}?e-ygsds|0w`oy|W~MMv0aL z`2{mvI@Mw$wEw1bKz}5=+RBnG$uspNUd-Rh$-uyv?&;zfQo(pP^zN+J3LIbLO*4~y z?*EVE&$_)?K1oQweCH?Ow!#L1kL+jP7k}K{lC1RSBMU>ZeceQf`uPW^f60heNRv~4 zSsA>setB!LccYS7C1d9_?ZB0j)XQ{ssuk~>yzrE*`p3|pciRuIo9`^cFya2f*FWFs zt;nBH_~7TN+aX?Y{wKcGH(WFSmb$_%+QMtwT6y;U$&4~j4J1|=stTnj?VGx~I{KKm zwv+z^_6BDC<_k9}A8`bw@+kzgiv76dA-np64#Q;ih4+psZHhd=r@&e@ZFj?>a=8U{ z+ur{2>=M;I=aRi_xiW*=^_a(P40Du}*Zk^xtLkv2c7bIVs?&Gt^PBF~{=!1>d5+JXF3>(^E^ZMt%``5@P=?(p z>5}Lo=|JJDJkFPsUaaC@xHd+VE%uG?#_k_757ho8u`W=*w77NUm;HzTF=~Dj>AP`p SA3HEk7(8A5T-G@yGywq51^_hx literal 0 HcmV?d00001 diff --git a/MusicPlayer/Assets.xcassets/Positive/Player/Player_Single'logo.imageset/Group_1597880488@3x.png b/MusicPlayer/Assets.xcassets/Positive/Player/Player_Single'logo.imageset/Group_1597880488@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..116a31c9fb8a739efa4cebdaf324c1845bfa664b GIT binary patch literal 796 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~Wq?nJ>wj#(y}$>XFp5~I?59mY zLxf9${DN;D6Kt2}yuhHlZ@M+Jk8*O!`XzcZB7w3@^`0({Ar*{ouih@&s>Q?bpn$8$ z?f(BnbGO)AVlJyUKi{8`XEpcauO3C03;IXpzwkTQd@@&fs+0CrCTJ$3&mYYtZGsP2 zJJOl+9lS0373bY}b-?6=Tfw@j37e&v83(vnRAfH!7{NI&#*Q zGh*7;@a}KRT7uV@N6YRq*||_q(Vxv+>&Gt{*NsbWY_{x5JZ-$taKfox@dZDX*48HE zweR_Ue9F&LWea!mpZ+jwhoe?UjSSb*XS;S!xOVW(*7E^BJ{pTkaNKX*vUrw(&V*V2 z53es?Re2*-wUWE^O3@US)D;d@*|W<%dxA6e$$BoWx^QvMmTAi;o?W?SYq{mzxsqH{ z3hWQR4_$h`g70~*mz>DN;IEy9^FOO?`E##m!jzcz2M_$3Q0Cp;_;1s<%M+?fm@=zo zF32l&Un+7SB_u&gnQ<0(R-pSYn_lQ_1+=s(%A^J#u}El&Y!rx z+*&%c zK6@XQNIkoJXw&|H9ac+}bIlW0MwyCRyxT7$J(Fdv{PLo*=2xbrcZ_OzFD1tMX*lg+ zVN8o`{u3s=VCJdmi)E!*4C-E9;hL&{zGeQD)rwF2SM&s@ZC`r0_Qt}XkbTit5^o-! z-gh+T-ISw~@*Pq$VuZ>}-dc#{_Lo1jHmP51IsNUAke<%#es7D`2=qCZD!g}6tGyq; zzd7g6M$J0rQ2p<@Cl;;MTX$Qa|1IxNuRo6emA}Zl6;#%!{BFGjOp6Sju6{1-oD!M< D=Pir; literal 0 HcmV?d00001 diff --git a/MusicPlayer/MP/Common/Extension(扩展)/Notification.swift b/MusicPlayer/MP/Common/Extension(扩展)/Notification.swift index 01bfdc4..ce47aed 100644 --- a/MusicPlayer/MP/Common/Extension(扩展)/Notification.swift +++ b/MusicPlayer/MP/Common/Extension(扩展)/Notification.swift @@ -71,10 +71,18 @@ extension NotificationCenter{ case positive_browses_reload ///列表数据已更新 case positive_list_reload + ///播放器状态变化 + case switch_player_status + ///弹出底部音乐模块 + case pup_bottom_show ///弹出音乐播放器 case pup_player_vc ///播放器页面更新 case positive_player_reload + ///用户切换播放器播放方式 + case player_type_switch + ///用户清空了歌单 + case player_delete_list } } } diff --git a/MusicPlayer/MP/Common/Macro(宏定义与全局量)/Macro.swift b/MusicPlayer/MP/Common/Macro(宏定义与全局量)/Macro.swift index b5dab02..703b444 100644 --- a/MusicPlayer/MP/Common/Macro(宏定义与全局量)/Macro.swift +++ b/MusicPlayer/MP/Common/Macro(宏定义与全局量)/Macro.swift @@ -112,4 +112,14 @@ func createLabel(_ text:String? = nil, font:UIFont, textColor:UIColor, textAlign label.numberOfLines = lines return label } - +///根据播放器状态将按钮的图片进行切换 +func switchPlayTypeBtnIcon(_ btn:UIButton) { + switch MP_PlayerManager.shared.getPlayType() { + case .normal://列表播放图案 + btn.setBackgroundImage(UIImage(named: "List_NormolPlay'logo"), for: .normal) + case .random://随机播放图案 + btn.setBackgroundImage(UIImage(named: "Player_Shuffle'logo"), for: .normal) + case .single://单曲循环图案 + btn.setBackgroundImage(UIImage(named: "Player_Single'logo"), for: .normal) + } +} diff --git a/MusicPlayer/MP/Common/Tool(工具封装)/MP_NetWorkManager.swift b/MusicPlayer/MP/Common/Tool(工具封装)/MP_NetWorkManager.swift index c8934e4..66baeb9 100644 --- a/MusicPlayer/MP/Common/Tool(工具封装)/MP_NetWorkManager.swift +++ b/MusicPlayer/MP/Common/Tool(工具封装)/MP_NetWorkManager.swift @@ -28,6 +28,10 @@ class MP_NetWorkManager: NSObject { private let next:String = "/next" ///播放器接口 private let player:String = "/player" + ///搜索建议接口 + private let suggestions:String = "/music/get_search_suggestions" + ///搜索接口 + private let search = "/search" //MARK: - 固定参数 //访问数据(首次首页预览时获得) private var visitorData:String? @@ -42,7 +46,7 @@ class MP_NetWorkManager: NSObject { return } //生成新参数 - var parameters:[String:Any] = [ + let parameters:[String:Any] = [ "ctoken":continuation, "continuation":continuation, "type":"next", @@ -127,6 +131,7 @@ class MP_NetWorkManager: NSObject { } //MARK: - API请求 extension MP_NetWorkManager { + //MARK: - 请求首页预览 ///向YouTubemusic请求预览/首页数据 func requestBrowseDatas() { //实行串行异步队列,进行多次请求。由于第一次之后的请求都必须携带对应的continuation编码,所以串行队列。直到最后一次请求的continuation值为空,销毁队列 @@ -193,6 +198,7 @@ extension MP_NetWorkManager { } } } + //MARK: - 请求列表专辑预览 /// 向YouTubemusic请求列表/专辑数据,该接口调用的同样是browse预览接口 /// - Parameters: /// - item: 需要查看的模块 @@ -205,7 +211,7 @@ extension MP_NetWorkManager { return } //设置参数,browseId与params参数是必定携带内容 - var parameters:[String:Any] = [ + let parameters:[String:Any] = [ "browseId":(item.browseItem.browseContent.browseId ?? ""), "params":(item.browseItem.browseContent.params ?? ""), "prettyPrint":"false", @@ -213,6 +219,7 @@ extension MP_NetWorkManager { "client":[ //web端 "clientName": "WEB_REMIX", + "visitorData":visitorData, //当前访问版本(日期值) "clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "platform":"DESKTOP", @@ -255,6 +262,8 @@ extension MP_NetWorkManager { } } } + + //MARK: - 请求列表专辑下一部分 ///请求Next列表(优先于Player) /// - Parameter item: 请求的预览实体 func requestNextList(_ item: MPPositive_BrowseItemViewModel, completion:@escaping(([MPPositive_SongItemModel]) -> Void)) { @@ -266,7 +275,7 @@ extension MP_NetWorkManager { return } //设置参数,videoId与params参数是必定携带内容 - var parameters:[String:Any] = [ + let parameters:[String:Any] = [ "playlistId":(item.browseItem.musicVideo.playListId ?? ""), "videoId":(item.browseItem.musicVideo.videoId ?? ""), "prettyPrint":"false", @@ -274,6 +283,7 @@ extension MP_NetWorkManager { "client":[ //web端 "clientName": "WEB_REMIX", + "visitorData":visitorData, //当前访问版本(日期值) "clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "platform":"DESKTOP", @@ -290,39 +300,6 @@ extension MP_NetWorkManager { completion(listSongs) } } - ///请求Next歌词/相关内容 - /// - Parameter item: 请求的预览实体 - func requestNextLyricsAndRelated(_ item: MPPositive_SongItemModel, completion:@escaping(((String?,String?)) -> Void)) { - //拼接出next路径 - let path = header+point+next - //设置url - guard let url = URL(string: path) else { - print("Url is Incorrect") - return - } - //设置参数,videoId与params参数是必定携带内容 - var parameters:[String:Any] = [ - "videoId":(item.videoId ?? ""), - "prettyPrint":"false", - "context":[ - "client":[ - //web端 - "clientName": "WEB_REMIX", - //当前访问版本(日期值) - "clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", - "platform":"DESKTOP", - //语言 - "hl":Language_first_local, - //地址 - "gl":Location_First - ] - ] - ] - //发送next列表歌词/相关内容请求 - requestPostNextLyricsAndRelated(url, parameters: parameters) { result in - completion(result) - } - } //请求next列表 private func requestPostNextList(_ url:URL, parameters:Parameters, completion:@escaping (([MPPositive_SongItemModel]) -> Void)) { //发送post请求 @@ -339,6 +316,40 @@ extension MP_NetWorkManager { } } } + ///请求Next歌词/相关内容 + /// - Parameter item: 请求的预览实体 + func requestNextLyricsAndRelated(_ item: MPPositive_SongItemModel, completion:@escaping(((String?,String?)) -> Void)) { + //拼接出next路径 + let path = header+point+next + //设置url + guard let url = URL(string: path) else { + print("Url is Incorrect") + return + } + //设置参数,videoId与params参数是必定携带内容 + let parameters:[String:Any] = [ + "videoId":(item.videoId ?? ""), + "prettyPrint":"false", + "context":[ + "client":[ + //web端 + "clientName": "WEB_REMIX", + "visitorData":visitorData, + //当前访问版本(日期值) + "clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", + "platform":"DESKTOP", + //语言 + "hl":Language_first_local, + //地址 + "gl":Location_First + ] + ] + ] + //发送next列表歌词/相关内容请求 + requestPostNextLyricsAndRelated(url, parameters: parameters) { result in + completion(result) + } + } //请求请求Next歌词/相关内容 private func requestPostNextLyricsAndRelated(_ url:URL, parameters:Parameters, completion:@escaping(((String?,String?)) -> Void)) { //发送post请求 @@ -355,6 +366,8 @@ extension MP_NetWorkManager { } } } + + //MARK: - 请求player播放资源 /// 请求Player(单曲/视频)播放资源 /// - Parameter item: 请求的预览实体 func requestPlayer(_ item: MPPositive_SongItemModel, completion:@escaping (([String]?, [String]?) -> Void)){ @@ -406,7 +419,7 @@ extension MP_NetWorkManager { } } } - + //MARK: - 请求歌词 /// 请求歌词 /// - Parameter lyricId: 歌词id func requestLyric(_ lyricId:String, completion:@escaping((String) -> Void)) { @@ -418,13 +431,14 @@ extension MP_NetWorkManager { return } //设置参数,browseId与params参数是必定携带内容 - var parameters:[String:Any] = [ + let parameters:[String:Any] = [ "browseId":lyricId, "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", + "visitorData":visitorData, //当前访问版本(日期值) "clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", "platform":"DESKTOP", @@ -453,6 +467,105 @@ extension MP_NetWorkManager { } } } + + //MARK: - 请求搜索建议 + /// 请求搜索建议 + /// - Parameter content: 用户输入的文本 + func requestSearchSuggestions(_ content:String, completion:@escaping(([[MPPositive_SearchSuggestionItemModel]]) -> Void)) { + //拼接路径 + let path = header+point+suggestions + //设置url + guard let url = URL(string: path) else { + print("Url is Incorrect") + return + } + //设置参数 + let parameters:[String:Any] = [ + "input":content, + "prettyPrint":"false", + "context":[ + "client":[ + //web端 + "clientName": "WEB_REMIX", + "visitorData":visitorData, + //当前访问版本(日期值) + "clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", + "platform":"DESKTOP", + //语言 + "hl":Language_first_local, + //地址 + "gl":Location_First + ] + ] + ] + requestPostSearchSuggestions(url, parameters: parameters) { result in + completion(result) + } + } + //请求搜索建议 + private func requestPostSearchSuggestions(_ url:URL, parameters:Parameters, completion:@escaping(([[MPPositive_SearchSuggestionItemModel]]) -> Void)) { + //发送post请求 + AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonSearchSuggestions.self) { [weak self] (response) in + guard let self = self else {return} + switch response.result { + case .success(let value): + parsingSearchSuggestions(value) { results in + completion(results) + } + case .failure(let error): + // 请求失败,处理错误 + print("Request failed: \(error)") + } + } + } + + /// 请求搜索结果 + /// - Parameter text: 用户请求文本 + func requestSearchResults(_ text:String) { + //拼接路径 + let path = header+point+search + //设置url + guard let url = URL(string: path) else { + print("Url is Incorrect") + return + } + //设置参数 + let parameters:[String:Any] = [ + "query":text, + "prettyPrint":"false", + "context":[ + "client":[ + //web端 + "clientName": "WEB_REMIX", + "visitorData":visitorData, + //当前访问版本(日期值) + "clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00", + "platform":"DESKTOP", + //语言 + "hl":Language_first_local, + //地址 + "gl":Location_First + ] + ] + ] + // + } + //请求搜索结果 + private func requestPostSearchResults(_ url:URL, parameters:Parameters) { + //发送post请求 + AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonSearchResults.self) { [weak self] (response) in + guard let self = self else {return} + switch response.result { + case .success(let value): + if let contents = value.contents?.tabbedSearchResultsRenderer?.tabs?.first?.tabRenderer?.content?.sectionListRenderer?.contents { + parsingSearchResults(contents) + } + case .failure(let error): + // 请求失败,处理错误 + print("Request failed: \(error)") + } + } + } } //MARK: - 数据解析 extension MP_NetWorkManager { @@ -581,16 +694,18 @@ extension MP_NetWorkManager { if let tab = tabs.first { //获取一张播放列表 for (index, content) in (tab.tabRenderer?.content?.musicQueueRenderer?.content?.playlistPanelRenderer?.contents ?? []).enumerated() { - //生成一个音乐实体,用来装填部分数据 - let song = MPPositive_SongItemModel() - song.index = index - song.title = content.playlistPanelVideoRenderer?.title?.runs?.reduce("", { $0 + ($1.text ?? "")}) - song.longBylineText = content.playlistPanelVideoRenderer?.longBylineText?.runs?.reduce("", { $0 + ($1.text ?? "")}) - song.lengthText = content.playlistPanelVideoRenderer?.lengthText?.runs?.reduce("", { $0 + ($1.text ?? "")}) - song.shortBylineText = content.playlistPanelVideoRenderer?.shortBylineText?.runs?.reduce("", { $0 + ($1.text ?? "")}) - song.reviewUrls = content.playlistPanelVideoRenderer?.thumbnail?.thumbnails?.map({$0.url ?? ""}) - song.videoId = content.playlistPanelVideoRenderer?.videoId - array.append(song) + if let playlistPanelVideoRenderer = content.playlistPanelVideoRenderer { + //生成一个音乐实体,用来装填部分数据 + let song = MPPositive_SongItemModel() + song.index = index + song.title = playlistPanelVideoRenderer.title?.runs?.reduce("", { $0 + ($1.text ?? "")}) + song.longBylineText = playlistPanelVideoRenderer.longBylineText?.runs?.reduce("", { $0 + ($1.text ?? "")}) + song.lengthText = playlistPanelVideoRenderer.lengthText?.runs?.reduce("", { $0 + ($1.text ?? "")}) + song.shortBylineText = playlistPanelVideoRenderer.shortBylineText?.runs?.reduce("", { $0 + ($1.text ?? "")}) + song.reviewUrls = playlistPanelVideoRenderer.thumbnail?.thumbnails?.map({$0.url ?? ""}) + song.videoId = playlistPanelVideoRenderer.videoId + array.append(song) + } } } } @@ -713,6 +828,60 @@ extension MP_NetWorkManager { return "" } + /// 解析搜索建议_SearchSuggestions + /// - Parameters: + /// - searchSuggestions: 需要解析搜索建议 + /// - completion: 回掉两组搜索建议组 + private func parsingSearchSuggestions(_ searchSuggestions:JsonSearchSuggestions, completion:@escaping([[MPPositive_SearchSuggestionItemModel]]) -> Void) { + if let contents = searchSuggestions.contents { + var sections:[[MPPositive_SearchSuggestionItemModel]] = [] + contents.forEach { section in + var suggestions:[MPPositive_SearchSuggestionItemModel] = [] + section.searchSuggestionsSectionRenderer?.contents?.forEach({ content in + //生成搜索建议模型 + let item = MPPositive_SearchSuggestionItemModel() + if let searchSuggestionRenderer = content.searchSuggestionRenderer { + //这里是搜索词条建议 + item.title = searchSuggestionRenderer.suggestion?.runs?.reduce("", { $0 + ($1.text ?? "")}) + }else if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer { + var reviewUrls:[String] = [] + //这里是搜索音乐建议 + musicResponsiveListItemRenderer.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.forEach({ thumbnail in + reviewUrls.append(thumbnail.url ?? "") + }) + item.reviewUrls = reviewUrls + if let flexColumns = musicResponsiveListItemRenderer.flexColumns { + for (index,flexColumn) in flexColumns.enumerated() { + if index == 0 { + //主标题 + item.title = flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) + }else { + //副标题 + item.subtitle = (item.subtitle ?? "") + (flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) ?? "") + } + } + } + } + suggestions.append(item) + }) + sections.append(suggestions) + } + completion(sections) + } + } + ///解析搜索结果_SearchResults + private func parsingSearchResults(_ contents:[JsonSearchResults.Contents.TabbedSearchResultsRenderer.Tab.TabRenderer.Content.SectionListRenderer.Content]) { + contents.forEach { content in + //判断当前模块是最佳结果还是其它模块 + if let musicCardShelfRenderer = content.musicCardShelfRenderer { + //当前是最佳结果 + + }else { + //当前是其他结果 + } + } + } + //MARK: - 解析具体内容形式 //解析musicResponsiveListItemRenderer(单曲/视频) private func parsingMusicResponsiveListItemRenderer(_ musicResponsiveListItemRenderer: RootMusicResponsiveListItemRenderer) -> MPPositive_BrowseItemModel { @@ -742,12 +911,6 @@ extension MP_NetWorkManager { browseContent.browseId = run.navigationEndpoint?.browseEndpoint?.browseId browseContent.params = run.navigationEndpoint?.browseEndpoint?.params } -// if run.navigationEndpoint?.browseEndpoint?.browseId != nil { -// browseContent.browseId = run.navigationEndpoint?.browseEndpoint?.browseId -// } -// if run.navigationEndpoint?.browseEndpoint?.params != nil { -// browseContent.params = run.navigationEndpoint?.browseEndpoint?.params -// } }) item.browseContent = browseContent } @@ -785,12 +948,6 @@ extension MP_NetWorkManager { browseContent.browseId = run.navigationEndpoint?.browseEndpoint?.browseId browseContent.params = run.navigationEndpoint?.browseEndpoint?.params } -// if run.navigationEndpoint?.browseEndpoint?.browseId != nil { -// browseContent.browseId = run.navigationEndpoint?.browseEndpoint?.browseId -// } -// if run.navigationEndpoint?.browseEndpoint?.params != nil { -// browseContent.params = run.navigationEndpoint?.browseEndpoint?.params -// } }) musicTwoRowItemRenderer.subtitle?.runs?.forEach({ run in if run.navigationEndpoint?.browseEndpoint?.browseId != nil { @@ -803,12 +960,6 @@ extension MP_NetWorkManager { browseContent.browseId = run.navigationEndpoint?.browseEndpoint?.browseId browseContent.params = run.navigationEndpoint?.browseEndpoint?.params } -// if run.navigationEndpoint?.browseEndpoint?.browseId != nil { -// browseContent.browseId = run.navigationEndpoint?.browseEndpoint?.browseId -// } -// if run.navigationEndpoint?.browseEndpoint?.params != nil { -// browseContent.params = run.navigationEndpoint?.browseEndpoint?.params -// } }) item.browseContent = browseContent if let playListId = musicTwoRowItemRenderer.thumbnailOverlay?.musicItemThumbnailOverlayRenderer?.content?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchPlaylistEndpoint?.playlistId { diff --git a/MusicPlayer/MP/Common/Tool(工具封装)/MP_PlayerManager.swift b/MusicPlayer/MP/Common/Tool(工具封装)/MP_PlayerManager.swift index d3165b1..6e2296d 100644 --- a/MusicPlayer/MP/Common/Tool(工具封装)/MP_PlayerManager.swift +++ b/MusicPlayer/MP/Common/Tool(工具封装)/MP_PlayerManager.swift @@ -43,21 +43,60 @@ typealias MP_PlayTimerStopAction = () -> Void ///播放器调整进度时执行事件 typealias MP_PlayTimerEditEndAction = () -> Void ///播放器 -class MP_PlayerManager{ +class MP_PlayerManager:NSObject{ ///控制器单例 static let shared = MP_PlayerManager() ///播放器 private var player:AVPlayer = AVPlayer() ///load模块 - var loadPlayer:MPPositive_PlayerLoadViewModel! + var loadPlayer:MPPositive_PlayerLoadViewModel!{ + didSet{ + if loadPlayer != nil { + //当load模块接受到新值的时候,发出通知,提醒底部模块状态切换 + NotificationCenter.notificationKey.post(notificationName: .pup_bottom_show) + }else { + //用户清空了load模块,隐藏播放器 + NotificationCenter.notificationKey.post(notificationName: .player_delete_list) + } + } + } //当前播放器状态 - private var playState:MP_PlayerStateType = .Null + private var playState:MP_PlayerStateType = .Null{ + didSet{ + //当播放器状态发生变化时,对播放器按钮状态进行切换 + NotificationCenter.notificationKey.post(notificationName: .switch_player_status, object: playState) + } + } + ///获取播放器播放状态 + func getPlayState() -> MP_PlayerStateType { + return playState + } + ///当前播放器播放方法 + private var playType:MP_PlayerPlayType = .normal{ + didSet{ + //当播放器播放方式变化后,发出通知 + NotificationCenter.notificationKey.post(notificationName: .player_type_switch) + } + } + ///获取播放器播放方法 + func getPlayType() -> MP_PlayerPlayType { + return playType + } + /// 设置播放器播放方式 + /// - Parameter type: 新的类型 + func setPlayType(_ type:MP_PlayerPlayType) { + playType = type + if playType == .random { + + } + } ///播放器启动时执行事件记录 private var startActionBlock:MP_PlayTimerStartAction! ///播放器运行时执行事件记录 - private var runActionBlock:MP_PlayTimerRunAction! + var runActionBlock:MP_PlayTimerRunAction! - private init() { + private override init() { + super.init() // 添加观察者,监听播放结束事件 NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(_ :)), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem) NotificationCenter.notificationKey.add(observer: self, selector: #selector(userSwitchCurrentVideoAction(_ :)), notificationName: .positive_player_reload) @@ -65,16 +104,17 @@ class MP_PlayerManager{ deinit { NotificationCenter.default.removeObserver(self) } - ///获取播放器播放状态 - func getPlayState() -> MP_PlayerStateType { - return playState - } /// 开始播放音乐 /// - Parameters: /// - startAction: 开始播放时需要执行的事件 /// - runAction: 播放途中需要执行的事件 /// - endAction: 结束播放时需要执行的事件 - func play(startAction:MP_PlayTimerStartAction? = nil, runAction:MP_PlayTimerRunAction? = nil) { + func play(startAction:MP_PlayTimerStartAction? = nil) { + guard loadPlayer != nil, loadPlayer.currentVideo != nil else { + //当两项数据皆为空时,播放器无法播放 + print("Player No Data") + return + } //检索播放器状态 switch playState { case .Null://未启动 @@ -88,10 +128,7 @@ class MP_PlayerManager{ if startAction != nil { startActionBlock = startAction } - if runAction != nil { - runActionBlock = runAction - } - //判断是否有PlayerItem + //覆盖播放器原有的playerItem player.replaceCurrentItem(with: loadPlayer.currentVideo.resourcePlayerItem) //将进度回归为0 player.seek(to: .zero) @@ -102,12 +139,6 @@ class MP_PlayerManager{ guard let self = self else { return } //转化为当前播放进度秒值 let currentDuration = CMTimeGetSeconds(time) - //当current为0时执行开始事件 - if currentDuration == 0 { - if startActionBlock != nil { - startActionBlock!() - } - } //获取当前播放音乐资源的最大时间值 let maxDuration = getMusicDuration() if maxDuration.isNaN == false { @@ -120,17 +151,65 @@ class MP_PlayerManager{ } } }) - //播放 - player.play() - playState = .Playing - //启动除了当前播放意外的预加载内容 -// let set = Set(loadPlayer.listViewVideos.filter({$0.index != loadPlayer.currentVideo.index})) -// set.forEach { item in -// if item.canBePreloaded() == false { -// item.preloadPlayerItem() -// } + //判断当前Video是否完成预加载 + if loadPlayer.currentVideo.isPreloading == true { + //已经完成了预加载 + print("开始播放音乐-\(loadPlayer.currentVideo.title ?? "")") + player.play() + playState = .Playing + //执行开始播放闭包 + if startActionBlock != nil { + startActionBlock!() + } + }else { + //未完成预加载,通过KVO来准确控制播放 + //为这个currentVideo的resourcePlayerItem创建KVO,分别监听这个item的status,playbackLikelyToKeepUp + loadPlayer.currentVideo.resourcePlayerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil) + loadPlayer.currentVideo.resourcePlayerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil) + } + //启动除了当前播放Video以外的Item的预加载内容 +// for item in loadPlayer.listViewVideos where item.song.videoId != loadPlayer.currentVideo.song.videoId { +// item.preloadPlayerItem() // } } + //实现KVO监听 + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + guard let keyPath = keyPath else { + return + } + //根据keyPath检索 + switch keyPath { + case "status"://playerItem状态 + if let statuValue = change?[.newKey] as? Int, statuValue == AVPlayerItem.Status.readyToPlay.rawValue { + //当statuVlaue值等于playerItem准备播放的值,说明已经准备好播放 + print("当前音乐-\(loadPlayer.currentVideo.title ?? "") 已经准备好播放") + }else { + print("当前音乐-\(loadPlayer.currentVideo.title ?? "") 未做好准备播放") + //当不能播放时,调整内容,再次播放 + } + case "playbackLikelyToKeepUp"://是否存在足够的数据开始播放 + if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true { + //播放器已经加载足够的数据,能够支撑播放 + print("当前音乐-\(loadPlayer.currentVideo.title ?? "") 有足够的缓存来播放") + //判断当前播放器是否在播放当前音乐中 + if playState != .Playing { + //还未播放当前音乐,启动播放 + print("开始播放音乐-\(loadPlayer.currentVideo.title ?? "")") + player.play() + playState = .Playing + //执行开始播放闭包 + if startActionBlock != nil { + startActionBlock!() + } + }else { + //播放器已经在播放了,不需要操作 + } + } + default: + break + } + } + //MARK: - 获取当前音乐总长度 ///获取音乐资源总时长 private func getMusicDuration() -> TimeInterval { return CMTimeGetSeconds(player.currentItem?.duration ?? .zero) @@ -143,8 +222,15 @@ class MP_PlayerManager{ guard playState == .Playing else { return } - //当前音乐播放器正在播放中,下一首 - nextEvent() + switch playType { + case .single: + playState = .Null + //重播 + player.seek(to: CMTime.zero) + default: + //当前音乐播放器正在播放中,下一首 + nextEvent() + } } //MARK: - 暂停播放 ///内部暂停播放 @@ -211,7 +297,7 @@ class MP_PlayerManager{ //MARK: - 停止播放 //停止播放 - private func stop() { + func stop() { //检索播放状态,是否已启动 guard playState != .Null else { //未启动 @@ -224,40 +310,101 @@ class MP_PlayerManager{ //MARK: - 切歌(上一首/下一首) ///上一首歌事件 func previousEvent() { - //判断是否存在上一首音乐 - let targetIndex = loadPlayer.listViewVideos.firstIndex(of: loadPlayer.currentVideo) - if targetIndex == 0 { - //当前音乐第一首,更新列表内容,获取最后一首歌,并播放 - let last = loadPlayer.songVideos.last - loadPlayer.improveData(last?.videoId ?? "") - }else { - //存在上一首,获取上一首ID,并播放 - let song = loadPlayer.songVideos.first(where: {$0.index == (loadPlayer.currentVideo.index-1)}) - loadPlayer.improveData(song?.videoId ?? "") + //将播放器状态调整未播放 + playState = .Null + var nextIndex:Int = 0 + //判断当前音乐播放方式 + switch playType { + case .random://随机,播放随机列表内容 + for (index, item) in loadPlayer.randomVideos.enumerated() { + if item.videoId == loadPlayer.currentVideo.song.videoId { + //找到播放音乐的索引 + nextIndex = index - 1 + } + } + //假如next为负数,则直接播放列表最后一首 + if nextIndex < 0 { + //播放列表最后一首 + let last = loadPlayer.randomVideos.last + loadPlayer.improveData(last?.videoId ?? "") + }else { + //查询列表对应单曲 + let song = loadPlayer.randomVideos[nextIndex] + loadPlayer.improveData(song.videoId ?? "") + } + default://常规播放或者单曲播放 + for (index, item) in loadPlayer.songVideos.enumerated() { + if item.videoId == loadPlayer.currentVideo.song.videoId { + //找到播放音乐的索引 + nextIndex = index - 1 + } + } + //假如next为负数,则直接播放列表最后一首 + if nextIndex < 0 { + //播放列表最后一首 + let last = loadPlayer.songVideos.last + loadPlayer.improveData(last?.videoId ?? "") + }else { + //查询列表对应单曲 + let song = loadPlayer.songVideos[nextIndex] + loadPlayer.improveData(song.videoId ?? "") + } } } ///下一首歌事件 func nextEvent() { - //判断是否存在下一首音乐 - let targetIndex = loadPlayer.listViewVideos.firstIndex(of: loadPlayer.currentVideo) - if targetIndex == (loadPlayer.listViewVideos.count - 1) { - //当前音乐最后一首,更新列表内容,获取第一首歌,并播放 - let first = loadPlayer.songVideos.first - loadPlayer.improveData(first?.videoId ?? "") - }else { - //存在下一首,获取下一首ID,并播放 - let song = loadPlayer.songVideos.first(where: {$0.index == (loadPlayer.currentVideo.index+1)}) - loadPlayer.improveData(song?.videoId ?? "") + //将播放器状态调整未播放 + playState = .Null + var nextIndex:Int = 0 + switch playType { + case .random: + for (index, item) in loadPlayer.randomVideos.enumerated() { + if item.videoId == loadPlayer.currentVideo.song.videoId { + //找到播放音乐的索引 + nextIndex = index + 1 + } + } + //超出播放列表数 + if nextIndex > (loadPlayer.randomVideos.count-1) { + //播放列表第一首 + let first = loadPlayer.randomVideos.first + loadPlayer.improveData(first?.videoId ?? "") + }else { + //存在下一首,获取下一首ID,并播放 + let song = loadPlayer.randomVideos[nextIndex] + loadPlayer.improveData(song.videoId ?? "") + } + default: + for (index, item) in loadPlayer.songVideos.enumerated() { + if item.videoId == loadPlayer.currentVideo.song.videoId { + //找到播放音乐的索引 + nextIndex = index + 1 + } + } + //超出播放列表数 + if nextIndex > (loadPlayer.songVideos.count-1) { + //播放列表第一首 + let first = loadPlayer.songVideos.first + loadPlayer.improveData(first?.videoId ?? "") + }else { + //存在下一首,获取下一首ID,并播放 + let song = loadPlayer.songVideos[nextIndex] + loadPlayer.improveData(song.videoId ?? "") + } } } ///监听到用户切换当前音乐 @objc private func userSwitchCurrentVideoAction(_ sender:Notification) { + //将播放器状态调整未播放 + playState = .Null + //优先获取传递的值 + if let video = sender.object as? MPPositive_SongViewModel { + video.resourcePlayerItem.removeObserver(self, forKeyPath: "status") + video.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") + } if loadPlayer.currentVideo != nil { //开始播放 - play(startAction: startActionBlock,runAction: runActionBlock) - }else { - //用户删除了音乐,播放下一首音乐 - + play(startAction: startActionBlock) } } diff --git a/MusicPlayer/MP/MPPositive/Models/JsonStructs/MPPositive_JsonSearchResults.swift b/MusicPlayer/MP/MPPositive/Models/JsonStructs/MPPositive_JsonSearchResults.swift new file mode 100644 index 0000000..7f66ced --- /dev/null +++ b/MusicPlayer/MP/MPPositive/Models/JsonStructs/MPPositive_JsonSearchResults.swift @@ -0,0 +1,622 @@ +// +// MPPositive_JsonSearchResults.swift +// MusicPlayer +// +// Created by Mr.Zhou on 2024/5/12. +// + +import UIKit + +///搜索结果结构 +struct JsonSearchResults: Codable { + let contents:Contents? + enum CodingKeys: String, CodingKey { + case contents = "contents" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + contents = try values.decodeIfPresent(Contents.self, forKey: .contents) + } + struct Contents: Codable { + let tabbedSearchResultsRenderer:TabbedSearchResultsRenderer? + enum CodingKeys: String, CodingKey { + case tabbedSearchResultsRenderer = "tabbedSearchResultsRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + tabbedSearchResultsRenderer = try values.decodeIfPresent(TabbedSearchResultsRenderer.self, forKey: .tabbedSearchResultsRenderer) + } + struct TabbedSearchResultsRenderer: Codable { + let tabs:[Tab]? + enum CodingKeys: String, CodingKey { + case tabs = "tabs" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + tabs = try values.decodeIfPresent([Tab].self, forKey: .tabs) + } + struct Tab:Codable { + let tabRenderer:TabRenderer? + enum CodingKeys: String, CodingKey { + case tabRenderer = "tabRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + tabRenderer = try values.decodeIfPresent(TabRenderer.self, forKey: .tabRenderer) + } + struct TabRenderer: Codable { + let content:Content? + enum CodingKeys: String, CodingKey { + case content = "content" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + content = try values.decodeIfPresent(Content.self, forKey: .content) + } + struct Content: Codable { + let sectionListRenderer:SectionListRenderer? + enum CodingKeys: String, CodingKey { + case sectionListRenderer = "sectionListRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + sectionListRenderer = try values.decodeIfPresent(SectionListRenderer.self, forKey: .sectionListRenderer) + } + struct SectionListRenderer: Codable { + ///模块(每个模块内容不一样) + let contents:[Content]? + enum CodingKeys: String, CodingKey { + case contents = "contents" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + contents = try values.decodeIfPresent([Content].self, forKey: .contents) + } + //MARK: - 模块 + struct Content: Codable { + ///最佳结果 + let musicCardShelfRenderer:MusicCardShelfRenderer? + ///其他结果 + let musicShelfRenderer:MusicShelfRenderer? + enum CodingKeys: String, CodingKey { + case musicCardShelfRenderer = "musicCardShelfRenderer" + case musicShelfRenderer = "musicShelfRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + musicCardShelfRenderer = try values.decodeIfPresent(MusicCardShelfRenderer.self, forKey: .musicCardShelfRenderer) + musicShelfRenderer = try values.decodeIfPresent(MusicShelfRenderer.self, forKey: .musicShelfRenderer) + } + //MARK: - 不同模块携带的内容 + ///最佳结果 + struct MusicCardShelfRenderer: Codable { + ///最佳结果封面 + let thumbnail:Thumbnail? + ///最佳结果标题(附带单曲ID) + let title:Title? + ///最佳结果副标题 + let subtitle:Subtitle? + ///最佳结果组头标题 + let header:Header? + ///最佳结果其他内容,第0位是无用数据,获取时跳过 + let contents:[Content]? + enum CodingKeys: String, CodingKey { + case thumbnail = "thumbnail" + case title = "title" + case subtitle = "subtitle" + case header = "header" + case contents = "contents" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail) + title = try values.decodeIfPresent(Title.self, forKey: .title) + subtitle = try values.decodeIfPresent(Subtitle.self, forKey: .subtitle) + header = try values.decodeIfPresent(Header.self, forKey: .header) + contents = try values.decodeIfPresent([Content].self, forKey: .contents) + } + ///最佳结果封面 + struct Thumbnail: Codable { + let musicThumbnailRenderer:MusicThumbnailRenderer? + enum CodingKeys: String, CodingKey { + case musicThumbnailRenderer = "musicThumbnailRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + musicThumbnailRenderer = try values.decodeIfPresent(MusicThumbnailRenderer.self, forKey: .musicThumbnailRenderer) + } + struct MusicThumbnailRenderer: Codable { + let thumbnail:Thumbnail? + enum CodingKeys: String, CodingKey { + case thumbnail = "thumbnail" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail) + } + struct Thumbnail: Codable { + let thumbnails:[Thumbnails]? + enum CodingKeys: String, CodingKey { + case thumbnails = "thumbnails" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + thumbnails = try values.decodeIfPresent([Thumbnails].self, forKey: .thumbnails) + } + struct Thumbnails: Codable { + let url:String? + enum CodingKeys: String, CodingKey { + case url = "url" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + url = try values.decodeIfPresent(String.self, forKey: .url) + } + } + } + } + } + ///最佳结果标题 + struct Title: Codable { + let runs:[Run]? + enum CodingKeys: String, CodingKey { + case runs = "runs" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + runs = try values.decodeIfPresent([Run].self, forKey: .runs) + } + struct Run: Codable { + ///标题文本 + let text:String? + let navigationEndpoint:NavigationEndpoint? + enum CodingKeys: String, CodingKey { + case text = "text" + case navigationEndpoint = "navigationEndpoint" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + text = try values.decodeIfPresent(String.self, forKey: .text) + navigationEndpoint = try values.decodeIfPresent(NavigationEndpoint.self, forKey: .navigationEndpoint) + } + struct NavigationEndpoint: Codable { + ///这个值存在就是音乐单曲 + let watchEndpoint:WatchEndpoint? + ///这个值存在就是艺术家 + let browseEndpoint:BrowseEndpoint? + enum CodingKeys: String, CodingKey { + case watchEndpoint = "watchEndpoint" + case browseEndpoint = "browseEndpoint" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + watchEndpoint = try values.decodeIfPresent(WatchEndpoint.self, forKey: .watchEndpoint) + browseEndpoint = try values.decodeIfPresent(BrowseEndpoint.self, forKey: .browseEndpoint) + } + struct WatchEndpoint: Codable { + let videoId:String? + enum CodingKeys: String, CodingKey { + case videoId = "videoId" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + videoId = try values.decodeIfPresent(String.self, forKey: .videoId) + } + } + struct BrowseEndpoint: Codable { + let browseId:String? + + enum CodingKeys: String, CodingKey { + case browseId = "browseId" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + browseId = try values.decodeIfPresent(String.self, forKey: .browseId) + } + } + } + } + } + ///最佳结果副标题 + struct Subtitle: Codable { + let runs:[Run]? + enum CodingKeys: String, CodingKey { + case runs = "runs" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + runs = try values.decodeIfPresent([Run].self, forKey: .runs) + } + struct Run: Codable { + ///标题文本 + let text:String? + enum CodingKeys: String, CodingKey { + case text = "text" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + text = try values.decodeIfPresent(String.self, forKey: .text) + } + } + } + ///最佳结果组头 + struct Header: Codable { + let musicCardShelfHeaderBasicRenderer:MusicCardShelfHeaderBasicRenderer? + enum CodingKeys: String, CodingKey { + case musicCardShelfHeaderBasicRenderer = "musicCardShelfHeaderBasicRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + musicCardShelfHeaderBasicRenderer = try values.decodeIfPresent(MusicCardShelfHeaderBasicRenderer.self, forKey: .musicCardShelfHeaderBasicRenderer) + } + struct MusicCardShelfHeaderBasicRenderer: Codable { + let title:Title? + enum CodingKeys: String, CodingKey { + case title = "title" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + title = try values.decodeIfPresent(Title.self, forKey: .title) + } + struct Title:Codable { + let runs:[Run]? + enum CodingKeys: String, CodingKey { + case runs = "runs" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + runs = try values.decodeIfPresent([Run].self, forKey: .runs) + } + + struct Run: Codable { + ///标题文本 + let text:String? + enum CodingKeys: String, CodingKey { + case text = "text" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + text = try values.decodeIfPresent(String.self, forKey: .text) + } + } + } + } + } + ///最佳结果其他内容 + struct Content: Codable { + let musicResponsiveListItemRenderer:MusicResponsiveListItemRenderer? + enum CodingKeys: String, CodingKey { + case musicResponsiveListItemRenderer = "musicResponsiveListItemRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + musicResponsiveListItemRenderer = try values.decodeIfPresent(MusicResponsiveListItemRenderer.self, forKey: .musicResponsiveListItemRenderer) + } + struct MusicResponsiveListItemRenderer: Codable { + ///封面 + let thumbnail:Thumbnail? + ///文本内容(第0位是标题,其他拼成副标题) + let flexColumns:[FlexColumn]? + ///单曲/视频ID(playlistItemData存在说明这条数据单曲/视频) + let playlistItemData:PlaylistItemData? + ///专辑/歌单ID(navigationEndpoint存在说明这条数据是专辑/歌单) + let navigationEndpoint:NavigationEndpoint? + enum CodingKeys: String, CodingKey { + case thumbnail = "thumbnail" + case flexColumns = "flexColumns" + case playlistItemData = "playlistItemData" + case navigationEndpoint = "navigationEndpoint" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail) + flexColumns = try values.decodeIfPresent([FlexColumn].self, forKey: .flexColumns) + playlistItemData = try values.decodeIfPresent(PlaylistItemData.self, forKey: .playlistItemData) + navigationEndpoint = try values.decodeIfPresent(NavigationEndpoint.self, forKey: .navigationEndpoint) + } + struct Thumbnail: Codable { + let musicThumbnailRenderer:MusicThumbnailRenderer? + enum CodingKeys: String, CodingKey { + case musicThumbnailRenderer = "musicThumbnailRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + musicThumbnailRenderer = try values.decodeIfPresent(MusicThumbnailRenderer.self, forKey: .musicThumbnailRenderer) + } + struct MusicThumbnailRenderer: Codable { + let thumbnail:Thumbnail? + enum CodingKeys: String, CodingKey { + case thumbnail = "thumbnail" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail) + } + struct Thumbnail: Codable { + let thumbnails:[Thumbnails]? + enum CodingKeys: String, CodingKey { + case thumbnails = "thumbnails" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + thumbnails = try values.decodeIfPresent([Thumbnails].self, forKey: .thumbnails) + } + struct Thumbnails: Codable { + let url:String? + enum CodingKeys: String, CodingKey { + case url = "url" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + url = try values.decodeIfPresent(String.self, forKey: .url) + } + } + } + } + } + struct FlexColumn: Codable { + let musicResponsiveListItemFlexColumnRenderer:MusicResponsiveListItemFlexColumnRenderer? + enum CodingKeys: String, CodingKey { + case musicResponsiveListItemFlexColumnRenderer = "musicResponsiveListItemFlexColumnRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + musicResponsiveListItemFlexColumnRenderer = try values.decodeIfPresent(MusicResponsiveListItemFlexColumnRenderer.self, forKey: .musicResponsiveListItemFlexColumnRenderer) + } + struct MusicResponsiveListItemFlexColumnRenderer: Codable { + let text:Text? + enum CodingKeys: String, CodingKey { + case text = "text" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + text = try values.decodeIfPresent(Text.self, forKey: .text) + } + struct Text: Codable { + let runs:[Run]? + enum CodingKeys: String, CodingKey { + case runs = "runs" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + runs = try values.decodeIfPresent([Run].self, forKey: .runs) + } + struct Run: Codable { + ///标题文本 + let text:String? + enum CodingKeys: String, CodingKey { + case text = "text" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + text = try values.decodeIfPresent(String.self, forKey: .text) + } + } + } + } + } + struct PlaylistItemData: Codable { + let videoId:String? + enum CodingKeys: String, CodingKey { + case videoId = "videoId" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + videoId = try values.decodeIfPresent(String.self, forKey: .videoId) + } + } + struct NavigationEndpoint: Codable { + let browseEndpoint:BrowseEndpoint? + enum CodingKeys: String, CodingKey { + case browseEndpoint = "browseEndpoint" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + browseEndpoint = try values.decodeIfPresent(BrowseEndpoint.self, forKey: .browseEndpoint) + } + struct BrowseEndpoint: Codable { + let browseId:String? + + enum CodingKeys: String, CodingKey { + case browseId = "browseId" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + browseId = try values.decodeIfPresent(String.self, forKey: .browseId) + } + } + } + } + } + } + ///其他结果 + struct MusicShelfRenderer: Codable { + ///模块标题 + let title:Title? + ///模块内容 + let contents:[Content]? + enum CodingKeys: String, CodingKey { + case title = "title" + case contents = "contents" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + title = try values.decodeIfPresent(Title.self, forKey: .title) + contents = try values.decodeIfPresent([Content].self, forKey: .contents) + } + struct Title:Codable { + let runs:[Run]? + enum CodingKeys: String, CodingKey { + case runs = "runs" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + runs = try values.decodeIfPresent([Run].self, forKey: .runs) + } + + struct Run: Codable { + ///标题文本 + let text:String? + enum CodingKeys: String, CodingKey { + case text = "text" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + text = try values.decodeIfPresent(String.self, forKey: .text) + } + } + } + struct Content: Codable { + let musicResponsiveListItemRenderer:MusicResponsiveListItemRenderer? + enum CodingKeys: String, CodingKey { + case musicResponsiveListItemRenderer = "musicResponsiveListItemRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + musicResponsiveListItemRenderer = try values.decodeIfPresent(MusicResponsiveListItemRenderer.self, forKey: .musicResponsiveListItemRenderer) + } + struct MusicResponsiveListItemRenderer: Codable { + ///封面 + let thumbnail:Thumbnail? + ///文本内容(第0位是标题,其他拼成副标题) + let flexColumns:[FlexColumn]? + ///单曲/视频ID(playlistItemData存在说明这条数据单曲/视频) + let playlistItemData:PlaylistItemData? + ///专辑/歌单ID(navigationEndpoint存在说明这条数据是专辑/歌单) + let navigationEndpoint:NavigationEndpoint? + enum CodingKeys: String, CodingKey { + case thumbnail = "thumbnail" + case flexColumns = "flexColumns" + case playlistItemData = "playlistItemData" + case navigationEndpoint = "navigationEndpoint" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail) + flexColumns = try values.decodeIfPresent([FlexColumn].self, forKey: .flexColumns) + playlistItemData = try values.decodeIfPresent(PlaylistItemData.self, forKey: .playlistItemData) + navigationEndpoint = try values.decodeIfPresent(NavigationEndpoint.self, forKey: .navigationEndpoint) + } + struct Thumbnail: Codable { + let musicThumbnailRenderer:MusicThumbnailRenderer? + enum CodingKeys: String, CodingKey { + case musicThumbnailRenderer = "musicThumbnailRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + musicThumbnailRenderer = try values.decodeIfPresent(MusicThumbnailRenderer.self, forKey: .musicThumbnailRenderer) + } + struct MusicThumbnailRenderer: Codable { + let thumbnail:Thumbnail? + enum CodingKeys: String, CodingKey { + case thumbnail = "thumbnail" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail) + } + struct Thumbnail: Codable { + let thumbnails:[Thumbnails]? + enum CodingKeys: String, CodingKey { + case thumbnails = "thumbnails" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + thumbnails = try values.decodeIfPresent([Thumbnails].self, forKey: .thumbnails) + } + struct Thumbnails: Codable { + let url:String? + enum CodingKeys: String, CodingKey { + case url = "url" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + url = try values.decodeIfPresent(String.self, forKey: .url) + } + } + } + } + } + struct FlexColumn: Codable { + let musicResponsiveListItemFlexColumnRenderer:MusicResponsiveListItemFlexColumnRenderer? + enum CodingKeys: String, CodingKey { + case musicResponsiveListItemFlexColumnRenderer = "musicResponsiveListItemFlexColumnRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + musicResponsiveListItemFlexColumnRenderer = try values.decodeIfPresent(MusicResponsiveListItemFlexColumnRenderer.self, forKey: .musicResponsiveListItemFlexColumnRenderer) + } + struct MusicResponsiveListItemFlexColumnRenderer: Codable { + let text:Text? + enum CodingKeys: String, CodingKey { + case text = "text" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + text = try values.decodeIfPresent(Text.self, forKey: .text) + } + struct Text: Codable { + let runs:[Run]? + enum CodingKeys: String, CodingKey { + case runs = "runs" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + runs = try values.decodeIfPresent([Run].self, forKey: .runs) + } + struct Run: Codable { + ///标题文本 + let text:String? + enum CodingKeys: String, CodingKey { + case text = "text" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + text = try values.decodeIfPresent(String.self, forKey: .text) + } + } + } + } + } + struct PlaylistItemData: Codable { + let videoId:String? + enum CodingKeys: String, CodingKey { + case videoId = "videoId" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + videoId = try values.decodeIfPresent(String.self, forKey: .videoId) + } + } + struct NavigationEndpoint: Codable { + let browseEndpoint:BrowseEndpoint? + enum CodingKeys: String, CodingKey { + case browseEndpoint = "browseEndpoint" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + browseEndpoint = try values.decodeIfPresent(BrowseEndpoint.self, forKey: .browseEndpoint) + } + struct BrowseEndpoint: Codable { + let browseId:String? + + enum CodingKeys: String, CodingKey { + case browseId = "browseId" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + browseId = try values.decodeIfPresent(String.self, forKey: .browseId) + } + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/MusicPlayer/MP/MPPositive/Models/JsonStructs/MPPositive_JsonSearchSuggestions.swift b/MusicPlayer/MP/MPPositive/Models/JsonStructs/MPPositive_JsonSearchSuggestions.swift new file mode 100644 index 0000000..59186a7 --- /dev/null +++ b/MusicPlayer/MP/MPPositive/Models/JsonStructs/MPPositive_JsonSearchSuggestions.swift @@ -0,0 +1,190 @@ +// +// MPPositive_JsonSearchSuggestions.swift +// MusicPlayer +// +// Created by Mr.Zhou on 2024/5/12. +// + +import UIKit + +struct JsonSearchSuggestions: Codable { + let contents:[Content]? + enum CodingKeys: String, CodingKey { + case contents = "contents" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + contents = try values.decodeIfPresent([Content].self, forKey: .contents) + } + struct Content: Codable { + let searchSuggestionsSectionRenderer:SearchSuggestionsSectionRenderer? + enum CodingKeys: String, CodingKey { + case searchSuggestionsSectionRenderer = "searchSuggestionsSectionRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + searchSuggestionsSectionRenderer = try values.decodeIfPresent(SearchSuggestionsSectionRenderer.self, forKey: .searchSuggestionsSectionRenderer) + } + struct SearchSuggestionsSectionRenderer: Codable { + let contents:[Content]? + enum CodingKeys: String, CodingKey { + case contents = "contents" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + contents = try values.decodeIfPresent([Content].self, forKey: .contents) + } + struct Content: Codable { + ///词条建议 + let searchSuggestionRenderer:SearchSuggestionRenderer? + ///音乐建议 + let musicResponsiveListItemRenderer:MusicResponsiveListItemRenderer? + enum CodingKeys: String, CodingKey { + case searchSuggestionRenderer = "searchSuggestionRenderer" + case musicResponsiveListItemRenderer = "musicResponsiveListItemRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + searchSuggestionRenderer = try values.decodeIfPresent(SearchSuggestionRenderer.self, forKey: .searchSuggestionRenderer) + musicResponsiveListItemRenderer = try values.decodeIfPresent(MusicResponsiveListItemRenderer.self, forKey: .musicResponsiveListItemRenderer) + } + //MARK: - 词条建议 + struct SearchSuggestionRenderer: Codable { + let suggestion:Suggestion? + enum CodingKeys: String, CodingKey { + case suggestion = "suggestion" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + suggestion = try values.decodeIfPresent(Suggestion.self, forKey: .suggestion) + } + struct Suggestion: Codable { + let runs:[Run]? + enum CodingKeys: String, CodingKey { + case runs = "runs" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + runs = try values.decodeIfPresent([Run].self, forKey: .runs) + } + struct Run: Codable { + let text:String? + enum CodingKeys: String, CodingKey { + case text = "text" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + text = try values.decodeIfPresent(String.self, forKey: .text) + } + } + } + } + //MARK: - 音乐建议 + struct MusicResponsiveListItemRenderer: Codable { + let thumbnail:Thumbnail? + let flexColumns:[FlexColumn]? + enum CodingKeys: String, CodingKey { + case thumbnail = "thumbnail" + case flexColumns = "flexColumns" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail) + flexColumns = try values.decodeIfPresent([FlexColumn].self, forKey: .flexColumns) + } + struct Thumbnail: Codable { + let musicThumbnailRenderer:MusicThumbnailRenderer? + enum CodingKeys: String, CodingKey { + case musicThumbnailRenderer = "musicThumbnailRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + musicThumbnailRenderer = try values.decodeIfPresent(MusicThumbnailRenderer.self, forKey: .musicThumbnailRenderer) + } + + struct MusicThumbnailRenderer: Codable { + let thumbnail:Thumbnail? + enum CodingKeys: String, CodingKey { + case thumbnail = "thumbnail" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + self.thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail) + } + struct Thumbnail: Codable { + ///封面图片组(默认取最后一位,图像内容最大最高清) + let thumbnails:[Thumbnails]? + enum CodingKeys: String, CodingKey { + case thumbnails = "thumbnails" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + self.thumbnails = try values.decodeIfPresent([Thumbnails].self, forKey: .thumbnails) + } + //MARK: - 封面图片 + ///封面图片 + struct Thumbnails: Codable { + ///封面图片路径 + let url:String? + let width:CGFloat? + let height:CGFloat? + enum CodingKeys: String, CodingKey { + case url = "url" + case width = "width" + case height = "height" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + url = try values.decodeIfPresent(String.self, forKey: .url) + width = try values.decodeIfPresent(CGFloat.self, forKey: .width) + height = try values.decodeIfPresent(CGFloat.self, forKey: .height) + } + } + } + } + } + struct FlexColumn: Codable { + let musicResponsiveListItemFlexColumnRenderer:MusicResponsiveListItemFlexColumnRenderer? + enum CodingKeys: String, CodingKey { + case musicResponsiveListItemFlexColumnRenderer = "musicResponsiveListItemFlexColumnRenderer" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + musicResponsiveListItemFlexColumnRenderer = try values.decodeIfPresent(MusicResponsiveListItemFlexColumnRenderer.self, forKey: .musicResponsiveListItemFlexColumnRenderer) + } + struct MusicResponsiveListItemFlexColumnRenderer: Codable { + let text:Text? + enum CodingKeys: String, CodingKey { + case text = "text" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + text = try values.decodeIfPresent(Text.self, forKey: .text) + } + struct Text: Codable { + let runs:[Run]? + enum CodingKeys: String, CodingKey { + case runs = "runs" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + runs = try values.decodeIfPresent([Run].self, forKey: .runs) + } + struct Run: Codable { + let text:String? + enum CodingKeys: String, CodingKey { + case text = "text" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + text = try values.decodeIfPresent(String.self, forKey: .text) + } + } + } + } + } + } + } + } + } +} diff --git a/MusicPlayer/MP/MPPositive/Models/Models/MPPositive_SearchSuggestionItemModel.swift b/MusicPlayer/MP/MPPositive/Models/Models/MPPositive_SearchSuggestionItemModel.swift new file mode 100644 index 0000000..a75d553 --- /dev/null +++ b/MusicPlayer/MP/MPPositive/Models/Models/MPPositive_SearchSuggestionItemModel.swift @@ -0,0 +1,17 @@ +// +// MPPositive_SearchSuggestionsModel.swift +// MusicPlayer +// +// Created by Mr.Zhou on 2024/5/12. +// + +import UIKit +///搜索建议模型 +class MPPositive_SearchSuggestionItemModel: NSObject { + ///主标题 + var title:String? + ///副标题(音乐推荐才有,内容五花八门,非主题内容拼接在一起) + var subtitle:String? + ///预览图路径组(音乐推荐才有) + var reviewUrls:[String]? +} diff --git a/MusicPlayer/MP/MPPositive/Models/ViewModels/ListViewModels/MPPositive_SongViewModel.swift b/MusicPlayer/MP/MPPositive/Models/ViewModels/ListViewModels/MPPositive_SongViewModel.swift index cf008fd..c8a3c85 100644 --- a/MusicPlayer/MP/MPPositive/Models/ViewModels/ListViewModels/MPPositive_SongViewModel.swift +++ b/MusicPlayer/MP/MPPositive/Models/ViewModels/ListViewModels/MPPositive_SongViewModel.swift @@ -23,7 +23,7 @@ class MPPositive_SongViewModel: NSObject { var lyrics:String? ///相关内容ID var relatedId:String? - ///是否完成本次预加载 + ///是否完成预加载 var isPreloading:Bool? ///是否收藏 var isCollection:Bool? @@ -39,10 +39,9 @@ class MPPositive_SongViewModel: NSObject { configure() } deinit { - if self.isKvo == true { - // 移除观察者 - self.resourcePlayerItem.removeObserver(self, forKeyPath: "status") - self.isKvo = false + if isKvo == true { + resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") + isKvo = false } } //数据配置 @@ -83,46 +82,41 @@ class MPPositive_SongViewModel: NSObject { } //MARK: - 资源预加载 //检测资源是否能被预加载 - func canBePreloaded() -> Bool { + private func canBePreloaded() -> Bool { return self.isPreloading ?? false } - //异步预加载 + ///异步预加载 func preloadPlayerItem() { - if isKvo == false { - //为playerItem添加监听 - self.resourcePlayerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil) - isKvo = true + //执行预加载 + guard canBePreloaded() == false else { + print("\(title ?? "")已经预加载了") + return } + print(resourcePlayerItem.status) //手动触发,以此加载数据 self.resourcePlayerItem.seek(to: .zero) {[weak self] _ in guard let self = self else {return} - // 使用 dispatchSource 监听属性变化 - let timer = DispatchSource.makeTimerSource(queue: .global(qos: .background)) - //一秒触发一次 - timer.schedule(deadline: .now(), repeating: .seconds(1)) - timer.setEventHandler{ - if self.resourcePlayerItem.isPlaybackLikelyToKeepUp { - //预加载完成 - self.isPreloading = true - if self.isKvo == true { - // 当预加载足够时,移除观察者 - self.resourcePlayerItem.removeObserver(self, forKeyPath: "status") - self.isKvo = false - timer.cancel() - } + //实行异步监听预加载 + DispatchQueue.global(qos: .background).async { + if self.isKvo == false { + //为playerItem添加监听 + self.resourcePlayerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil) + self.isKvo = true } } - timer.resume() } } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - if keyPath == "status" { - if let status = change?[.newKey] as? Int, status == Int(AVPlayerItem.Status.readyToPlay.rawValue) { - // 播放准备就绪 - print("\(self.title ?? "") is Ok") - }else { - //资源无法播放 - print("\(self.title ?? "") is bad") + if keyPath == "playbackLikelyToKeepUp" { + if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true { + //当前资源已经预加载到合适的程度,移除KVO监听 + print("\(title ?? "")预加载到合适的进度") + if isKvo == true { + resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") + isKvo = false + } + //表示已经完成了预加载 + isPreloading = true } } } diff --git a/MusicPlayer/MP/MPPositive/Models/ViewModels/LoadViewModels/MPPositive_PlayerLoadViewModel.swift b/MusicPlayer/MP/MPPositive/Models/ViewModels/LoadViewModels/MPPositive_PlayerLoadViewModel.swift index a09cd9c..e9439f5 100644 --- a/MusicPlayer/MP/MPPositive/Models/ViewModels/LoadViewModels/MPPositive_PlayerLoadViewModel.swift +++ b/MusicPlayer/MP/MPPositive/Models/ViewModels/LoadViewModels/MPPositive_PlayerLoadViewModel.swift @@ -8,14 +8,21 @@ import UIKit ///播放器管理ViewModel class MPPositive_PlayerLoadViewModel: NSObject { - /// 单曲列表 + /// 单曲常规列表 var songVideos:[MPPositive_SongItemModel]! + ///随机播放列表 + var randomVideos:[MPPositive_SongItemModel]! ///当前播放音乐ViewModel var currentVideo:MPPositive_SongViewModel!{ - didSet{ - if currentVideo != nil { - //当值变化时通知播放器页面,更新UI - NotificationCenter.notificationKey.post(notificationName: .positive_player_reload) + willSet{ + if newValue != nil { + if currentVideo != nil { + //当值变化时通知播放器页面,更新UI + NotificationCenter.notificationKey.post(notificationName: .positive_player_reload, object: currentVideo) + }else { + //当值变化时通知播放器页面,更新UI + NotificationCenter.notificationKey.post(notificationName: .positive_player_reload) + } } } } @@ -31,35 +38,39 @@ class MPPositive_PlayerLoadViewModel: NSObject { /// - firstVideoId: 需要播放的第一首歌 init(_ songs:[MPPositive_SongItemModel], currentVideoId: String) { super.init() - //清空数据 self.songVideos = songs + //根据列表生成一份随机播放列表 + self.randomVideos = self.songVideos.shuffled() self.listViewVideos = [] self.currentVideoId = currentVideoId } ///将选中Video的上下2项包括本身总计3项Video进行补全转为ViewModel,并播放这首音乐 func improveData(_ targetVideoId:String) { - guard let targetVideo = self.songVideos.first(where: {$0.videoId == targetVideoId}) else { + //获取targetVideoId的索引 + guard let targetIndex = self.songVideos.firstIndex(where: {$0.videoId == targetVideoId}) else { return } //对于选中Video的集合 var array:[MPPositive_SongItemModel] = [] + array.append(self.songVideos[targetIndex]) //获取上一位 - if let previous = self.songVideos.first(where: {$0.index == (targetVideo.index-1)}) { - array.append(previous) + let previousIndex = targetIndex-1 + if previousIndex >= 0 { + array.append(self.songVideos[previousIndex]) } - array.append(targetVideo) - //获取下一位 - if let next = self.songVideos.first(where: {$0.index == (targetVideo.index+1)}) { - array.append(next) + let nextIndex = targetIndex+1 + if nextIndex < songVideos.count { + array.append(self.songVideos[nextIndex]) } //获取完成,优先检索ViewModel,看看是否已存在补完video let videoIDs = Set(listViewVideos.map({$0.song.videoId})) //比较videoID,去掉已经补完的内容 array = array.filter({!videoIDs.contains($0.videoId)}) group = DispatchGroup() + var numbers = 0 //去重完毕,对剩下内容补完 - array.forEach { item in + for item in array { group?.enter() //补全歌词id和相关内容id improveDataforLycirsAndRelated(item) {[weak self] (result) in @@ -82,41 +93,28 @@ class MPPositive_PlayerLoadViewModel: NSObject { self.listViewVideos = self.listViewVideos.sorted(by: {$0.index < $1.index}) //排序完成,确定播放音乐 self.currentVideo = self.listViewVideos.first(where: {$0.song.videoId == targetVideoId}) - + self.group = nil }) } ///移除选中的song,并更新listViewVideos,移除相同index的值 func removeData(_ targetVideoId:String) { - let targetIndex = songVideos.firstIndex(where: {$0.videoId == targetVideoId}) + let targetIndex = songVideos.firstIndex(where: {$0.videoId == targetVideoId}) ?? 0 //将选中的音乐移除,同时更新listView songVideos = songVideos.filter({$0.videoId != targetVideoId}) + randomVideos = randomVideos.filter({$0.videoId != targetVideoId}) listViewVideos = listViewVideos.filter({$0.song.videoId != targetVideoId}) - //更新下标/索引 - for (index, item) in songVideos.enumerated() { - item.index = index - } - listViewVideos.forEach { listModel in - songVideos.forEach { song in - if listModel.song.videoId == song.videoId { - listModel.index = song.index - listModel.song.index = song.index + if currentVideo != nil { + //判断是否当前音乐 + if currentVideo.song.videoId == targetVideoId { + //判断targetIndex是否大于最大音乐值 + if targetIndex < songVideos.count { + let videoId = songVideos[targetIndex].videoId ?? "" + improveData(videoId) + }else { + //移除的是原来最后一首音乐,播放新的最后一首音乐 + let videoId = songVideos.last?.videoId ?? "" + improveData(videoId) } - if currentVideo.song.videoId == song.videoId { - currentVideo.index = song.index - currentVideo.song.index = song.index - } - } - } - - //判断是否当前音乐 - if currentVideo.song.videoId == targetVideoId { - if let videoId = songVideos.first(where: {$0.index == targetIndex})?.videoId { - //更新当前音乐 - improveData(videoId) - }else { - //移除的是原来最后一首音乐,播放新的最后一首音乐 - let videoId = songVideos.last?.videoId ?? "" - improveData(videoId) } } } diff --git a/MusicPlayer/MP/MPPositive/ViewControllers/Base(基类,导航栏,标签栏)/MPPositive_TabBarController.swift b/MusicPlayer/MP/MPPositive/ViewControllers/Base(基类,导航栏,标签栏)/MPPositive_TabBarController.swift index a1d249d..0fd030a 100644 --- a/MusicPlayer/MP/MPPositive/ViewControllers/Base(基类,导航栏,标签栏)/MPPositive_TabBarController.swift +++ b/MusicPlayer/MP/MPPositive/ViewControllers/Base(基类,导航栏,标签栏)/MPPositive_TabBarController.swift @@ -7,7 +7,7 @@ import UIKit ///b面tabBar控制器 -class MPPositive_TabBarController: UITabBarController { +class MPPositive_TabBarController: UITabBarController, UIViewControllerTransitioningDelegate { //自定义tabBar private lazy var customTabBar:MPPositive_CustomTabBar = .init(frame: .init(x: 0, y: 0, width: screen_Width, height: 72*width)) private lazy var bottomView:MPPositive_BottomShowView = .init(frame: .init(x: 0, y: 0, width: 351, height: 82)) @@ -36,6 +36,17 @@ class MPPositive_TabBarController: UITabBarController { make.width.equalTo(351*width) make.height.equalTo(82*width) } + bottomView.showListBlock = { + [weak self] in + if MP_PlayerManager.shared.loadPlayer != nil { + MPPositive_ModalType = .PlayerList + let listVC = MPPositive_PlayerListShowViewController() + listVC.transitioningDelegate = self + listVC.modalPresentationStyle = .custom + self?.present(listVC, animated: true) + } + } + bottomView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(pupPlayerAction))) addNotification() } //监听通知 @@ -43,12 +54,19 @@ class MPPositive_TabBarController: UITabBarController { //监听标签切换 NotificationCenter.notificationKey.add(observer: self, selector: #selector(switchAction(_:)), notificationName: .switch_tabBarItem) //监听控制器弹出 - NotificationCenter.notificationKey.add(observer: self, selector: #selector(pupPlayerAction(_:)), notificationName: .pup_player_vc) + NotificationCenter.notificationKey.add(observer: self, selector: #selector(pupPlayerAction), notificationName: .pup_player_vc) + NotificationCenter.notificationKey.add(observer: self, selector: #selector(bottomAnimationAction(_:)), notificationName: .pup_bottom_show) + NotificationCenter.notificationKey.add(observer: self, selector: #selector(bottomAnimationAction(_:)), notificationName: .player_delete_list) } deinit { //移除所有监听 NotificationCenter.default.removeObserver(self) } + func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { + return MPPositive_PresentationController(presentedViewController: presented, presenting: presenting) + } + //弹出音乐播放器 + } //MARK: - 通知处理 extension MPPositive_TabBarController { @@ -58,12 +76,33 @@ extension MPPositive_TabBarController { selectedIndex = tag } //弹出player控制器 - @objc private func pupPlayerAction(_ sender:Notification) { - //检索播放器中是否存在load模型 - if MP_PlayerManager.shared.loadPlayer != nil { - let playerVC = MPPositive_PlayerViewController() - playerVC.modalPresentationStyle = .fullScreen - present(playerVC, animated: true) + @objc private func pupPlayerAction() { + DispatchQueue.main.async { + [weak self] in + //检索播放器中是否存在load模型 + if MP_PlayerManager.shared.loadPlayer != nil { + let playerVC = MPPositive_PlayerViewController() + playerVC.modalPresentationStyle = .fullScreen + self?.present(playerVC, animated: true) + } + } + } + //切换底部音乐模块状态 + @objc private func bottomAnimationAction(_ sender:Notification) { + switch_bottomShowAnimation(MP_PlayerManager.shared.loadPlayer != nil) + } + //底部BottomView的切换动画 + private func switch_bottomShowAnimation(_ state:Bool) { + UIView.animate(withDuration: 0.3) { + [weak self] in + guard let self = self else { return } + if state { + //向上展示 + bottomView.transform = .init(translationX: 0, y: -145*width) + }else { + //向下隐藏 + bottomView.transform = .identity + } } } } diff --git a/MusicPlayer/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerListShowViewController.swift b/MusicPlayer/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerListShowViewController.swift index 6739227..41a7d6c 100644 --- a/MusicPlayer/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerListShowViewController.swift +++ b/MusicPlayer/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerListShowViewController.swift @@ -31,8 +31,12 @@ class MPPositive_PlayerListShowViewController: UIViewController { view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] view.layer.cornerRadius = 18*width configure() + //添加监听,当前音乐切换后,刷新 + NotificationCenter.notificationKey.add(observer: self, selector: #selector(currentVideoReloadAction(_:)), notificationName: .positive_player_reload) + } + deinit { + NotificationCenter.default.removeObserver(self) } - private func configure() { view.addSubview(indictorImageView) indictorImageView.snp.makeConstraints { make in @@ -51,6 +55,10 @@ class MPPositive_PlayerListShowViewController: UIViewController { super.viewWillAppear(animated) tableView.reloadData() } + @objc private func currentVideoReloadAction(_ sender:Notification) { + tableView.reloadData() + } + } //MARK: - tableView extension MPPositive_PlayerListShowViewController: UITableViewDataSource, UITableViewDelegate { @@ -63,9 +71,16 @@ extension MPPositive_PlayerListShowViewController: UITableViewDataSource, UITabl cell.song = MP_PlayerManager.shared.loadPlayer.songVideos[indexPath.row] cell.removeBlock = { [weak self] in - //从列表中移除音乐 - MP_PlayerManager.shared.loadPlayer.removeData(MP_PlayerManager.shared.loadPlayer.songVideos[indexPath.row].videoId) - tableView.reloadData() + if MP_PlayerManager.shared.loadPlayer.songVideos.count > 1 { + //从列表中移除音乐 + MP_PlayerManager.shared.loadPlayer.removeData(MP_PlayerManager.shared.loadPlayer.songVideos[indexPath.row].videoId) + tableView.reloadData() + }else { + //当音乐库只有一首的话,移除的话,直接关闭播放器 + MP_PlayerManager.shared.stop() + MP_PlayerManager.shared.loadPlayer = nil + self?.dismiss(animated: true) + } } return cell } diff --git a/MusicPlayer/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerViewController.swift b/MusicPlayer/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerViewController.swift index e48ec22..2c438fb 100644 --- a/MusicPlayer/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerViewController.swift +++ b/MusicPlayer/MP/MPPositive/ViewControllers/Player(播放器)/MPPositive_PlayerViewController.swift @@ -74,6 +74,8 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont btn.setBackgroundImage(UIImage(named: "Player_Pause'logo"), for: .normal) btn.setBackgroundImage(UIImage(named: "Player_Player'logo"), for: .selected) btn.addTarget(self, action: #selector(playClick(_ :)), for: .touchUpInside) + //默认无法交互(以免用户交互导致网络请求混乱) + btn.isUserInteractionEnabled = false return btn }() //歌单列表按钮 @@ -87,7 +89,6 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont private lazy var typeBtn:UIButton = { let btn = UIButton() btn.setBackgroundImage(UIImage(named: "List_NormolPlay'logo"), for: .normal) - btn.setBackgroundImage(UIImage(named: "List_ShufflePlay'logo"), for: .selected) btn.addTarget(self, action: #selector(typeClick(_ :)), for: .touchUpInside) return btn }() @@ -115,10 +116,43 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont configure() //添加监听 NotificationCenter.notificationKey.add(observer: self, selector: #selector(playerReloadAction(_ :)), notificationName: .positive_player_reload) + NotificationCenter.notificationKey.add(observer: self, selector: #selector(statusSwitchAction(_:)), notificationName: .switch_player_status) + NotificationCenter.notificationKey.add(observer: self, selector: #selector(playerTypeSwitchAction(_:)), notificationName: .player_type_switch) + NotificationCenter.notificationKey.add(observer: self, selector: #selector(deleteListAction(_:)), notificationName: .player_delete_list) + //打开时检索播放器状态,好调整内容 + MP_PlayerManager.shared.runActionBlock = { [weak self] (currentTime, duration) in + guard let self = self else { return } + //展示当前时间 + coverView.durationLabel.text = setTimesToMinSeconds(currentTime) + //展示剩余时间 + let remain:TimeInterval = duration - currentTime + coverView.maxTimesLabel.text = setTimesToMinSeconds(remain) + //调整进度条内容 + let value = currentTime/duration + coverView.sliderView.value = Float(value) + } + switch MP_PlayerManager.shared.getPlayState() { + case .Null://说明播放器还未尝试过播放 + playBtn.isSelected = false + playBtn.isUserInteractionEnabled = false + case .Playing://播放中 + playBtn.isSelected = true + playBtn.isUserInteractionEnabled = true + default://暂停中 + playBtn.isSelected = false + playBtn.isUserInteractionEnabled = true + } } deinit { NotificationCenter.default.removeObserver(self) } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + //判断播放器是否装填数据 + if MP_PlayerManager.shared.loadPlayer.currentVideo != nil { + uploadUI() + } + } //视图配置 private func configure() { //导航View内容配置 @@ -251,39 +285,70 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont } return bottomView } + //MARK: - 页面渲染 + private func uploadUI() { + //填充数据 + backImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer.currentVideo?.coverUrl) + coverView.coverImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer.currentVideo?.coverUrl) + coverView.titleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.title + coverView.subtitleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.subtitle + lyricsView.titleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.title + lyricsView.subtitleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.subtitle + lyricsView.lyricsLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo.lyrics ?? "No Lyrics" + } //MARK: - 通知 + //播放器音乐刷新 @objc private func playerReloadAction(_ sender:Notification) { //渲染页面 DispatchQueue.main.async { [weak self] in guard let self = self else {return} - backImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer.currentVideo?.coverUrl) - coverView.coverImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer.currentVideo?.coverUrl) - coverView.titleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.title - coverView.subtitleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.subtitle - lyricsView.titleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.title - lyricsView.subtitleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.subtitle - lyricsView.lyricsLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo.lyrics ?? "No Lyrics" - //启动播放 + uploadUI() + //回正进度条 + coverView.sliderView.value = 0 + //调整时间值 + coverView.durationLabel.text = setTimesToMinSeconds(0) + //调整最大时间值 + coverView.maxTimesLabel.text = setTimesToMinSeconds(0) + //开始播放 MP_PlayerManager.shared.play { [weak self] in guard let self = self else { return } - //回正进度条 - coverView.sliderView.value = 0 - playBtn.isSelected = true + //允许playBtn按钮交互 playBtn.isUserInteractionEnabled = true - } runAction: { [weak self] (currentTime, duration) in - guard let self = self else { return } - //展示当前时间 - coverView.durationLabel.text = setTimesToMinSeconds(currentTime) - //展示剩余时间 - let remain:TimeInterval = duration - currentTime - coverView.maxTimesLabel.text = setTimesToMinSeconds(remain) - //调整进度条内容 - let value = currentTime/duration - coverView.sliderView.value = Float(value) } } } + //切换播放器状态时 + @objc private func statusSwitchAction(_ sender:Notification) { + if sender.object != nil { + let state:MP_PlayerStateType = sender.object as! MP_PlayerStateType + DispatchQueue.main.async { + [weak self] in + switch state { + case .Playing: + self?.playBtn.isSelected = true + default: + self?.playBtn.isSelected = false + } + } + } + } + //切换播放器播放方式 + @objc private func playerTypeSwitchAction(_ sender:Notification) { + DispatchQueue.main.async { + [weak self] in + guard let self = self else {return} + switchPlayTypeBtnIcon(typeBtn) + } + } + //用户清空了歌单列表 + @objc private func deleteListAction(_ sender:Notification) { + DispatchQueue.main.async { + [weak self] in + guard let self = self else {return} + dismiss(animated: true) + } + } //MARK: - 点击事件 //向下dismiss @objc private func disMissClick(_ sender:UIButton) { @@ -343,6 +408,10 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont //播放/暂停/继续 @objc private func playClick(_ sender:UIButton) { + guard MP_PlayerManager.shared.loadPlayer != nil else { + return + } + //在当前音乐填充好之前,禁止触发点击 switch MP_PlayerManager.shared.getPlayState() { case .Null: //启动播放 @@ -350,36 +419,23 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont guard let self = self else { return } //回正进度条 coverView.sliderView.value = 0 - sender.isSelected = true - } runAction: { [weak self] (currentTime, duration) in - guard let self = self else { return } - //展示当前时间 - coverView.durationLabel.text = setTimesToMinSeconds(currentTime) - //展示剩余时间 - let remain:TimeInterval = duration - currentTime - coverView.maxTimesLabel.text = setTimesToMinSeconds(remain) - //调整进度条内容 - let value = currentTime/duration - coverView.sliderView.value = Float(value) } - case .Playing: //播放中,进入暂停 MP_PlayerManager.shared.pause { [weak self] in - sender.isSelected = false } case .Pause: //暂停中,进入继续 MP_PlayerManager.shared.resume { [weak self] in - sender.isSelected = true } } } //展示列表 @objc private func listClick(_ sender:UIButton) { if MP_PlayerManager.shared.loadPlayer != nil { + MPPositive_ModalType = .PlayerList let listVC = MPPositive_PlayerListShowViewController() listVC.transitioningDelegate = self listVC.modalPresentationStyle = .custom @@ -391,12 +447,17 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont } //切换播放器状态(按顺序/随机/单曲) @objc private func typeClick(_ sender:UIButton) { - + //对播放器播放方式截形切换 + var value = MP_PlayerManager.shared.getPlayType().rawValue + value += 1 + if value > 2 { + value = 0 + } + MP_PlayerManager.shared.setPlayType(.init(rawValue: value)!) } //下一首 @objc private func nextClick(_ sender:UIButton) { coverView.sliderView.value = 0 - playBtn.isSelected = false playBtn.isUserInteractionEnabled = false MP_PlayerManager.shared.nextEvent() @@ -404,7 +465,6 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont //上一首 @objc private func previousClick(_ sender:UIButton) { coverView.sliderView.value = 0 - playBtn.isSelected = false playBtn.isUserInteractionEnabled = false MP_PlayerManager.shared.previousEvent() } diff --git a/MusicPlayer/MP/MPPositive/ViewControllers/Search(搜索页)/MPPositive_SearchResultShowViewController.swift b/MusicPlayer/MP/MPPositive/ViewControllers/Search(搜索页)/MPPositive_SearchResultShowViewController.swift new file mode 100644 index 0000000..f681481 --- /dev/null +++ b/MusicPlayer/MP/MPPositive/ViewControllers/Search(搜索页)/MPPositive_SearchResultShowViewController.swift @@ -0,0 +1,18 @@ +// +// MPPositive_SearchResultShowViewController.swift +// MusicPlayer +// +// Created by Mr.Zhou on 2024/5/12. +// + +import UIKit +///搜索结果控制器 +class MPPositive_SearchResultShowViewController: MPPositive_BaseViewController { + + override func viewDidLoad() { + super.viewDidLoad() + setTitle("Result") + setPopBtn() + } + +} diff --git a/MusicPlayer/MP/MPPositive/ViewControllers/Search(搜索页)/MPPositive_SearchViewController.swift b/MusicPlayer/MP/MPPositive/ViewControllers/Search(搜索页)/MPPositive_SearchViewController.swift index eb71d7d..20a9c1b 100644 --- a/MusicPlayer/MP/MPPositive/ViewControllers/Search(搜索页)/MPPositive_SearchViewController.swift +++ b/MusicPlayer/MP/MPPositive/ViewControllers/Search(搜索页)/MPPositive_SearchViewController.swift @@ -7,23 +7,201 @@ import UIKit -class MPPositive_SearchViewController: UIViewController { - +class MPPositive_SearchViewController: MPPositive_BaseViewController { + //背景图片 + private lazy var bgImageView:UIImageView = { + let imageView:UIImageView = .init(image: .init(named: "B_Home_BG'bg")) + imageView.contentMode = .scaleAspectFill + return imageView + }() + //顶部搜索textField + private lazy var searchTextField:UITextField = { + let textField = UITextField() + textField.delegate = self + textField.font = .systemFont(ofSize: 14*width, weight: .regular) + textField.textColor = .white + //设置一个富文本占位符 + let attributedText = NSAttributedString(string: "Search songs,artists,playlists", attributes: [.font:UIFont.systemFont(ofSize: 14*width, weight: .regular), .foregroundColor:UIColor(hex: "#666666")]) + textField.attributedPlaceholder = attributedText + return textField + }() + //搜索建议tableView + private lazy var suggestionsTableView:UITableView = { + let tableView = UITableView() + tableView.backgroundColor = .init(hex: "#151718") + tableView.separatorStyle = .none + //设置一个边框 + tableView.layer.borderWidth = 1*width + tableView.layer.borderColor = UIColor(hex: "#FFFFFF", alpha: 0.35).cgColor + tableView.layer.masksToBounds = true + tableView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + tableView.layer.cornerRadius = 10*width + + tableView.rowHeight = 60*width + tableView.dataSource = self + tableView.delegate = self + tableView.register(MPPositive_SearchSuggestionItemTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchSuggestionItemTableViewCellID) + tableView.register(MPPositive_SearchSuggestionListTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchSuggestionListTableViewCellID) + return tableView + }() + private let MPPositive_SearchSuggestionItemTableViewCellID = "MPPositive_SearchSuggestionItemTableViewCell" + private let MPPositive_SearchSuggestionListTableViewCellID = "MPPositive_SearchSuggestionListTableViewCell" + //对用户展示的搜索建议组 + private var suggestions:[[MPPositive_SearchSuggestionItemModel]]!{ + willSet{ + DispatchQueue.main.async { + [weak self] in + guard let self = self else {return} + if newValue != nil { + //搜索到了 + var count:Int = 0 + newValue.forEach { items in + count += items.count + } + suggestionsTableView.isHidden = false + suggestionsTableView.snp.updateConstraints { make in + make.height.equalTo(CGFloat(count*60)*width) + } + }else { + //没有搜索到 + suggestionsTableView.isHidden = true + suggestionsTableView.snp.updateConstraints { make in + make.height.equalTo(0) + } + } + suggestionsTableView.reloadData() + } + } + } + //搜索限定计时器 + private var debounceTimer: Timer? override func viewDidLoad() { super.viewDidLoad() - - // Do any additional setup after loading the view. + setTitle("") + configure() + } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + searchTextField.text = "" + suggestionsTableView.isHidden = true + suggestions = nil + } + //配置 + private func configure() { + let searchView = createSearchView() + navView.addSubview(searchView) + searchView.snp.makeConstraints { make in + make.width.equalTo(339*width) + make.height.equalTo(32*width) + make.center.equalToSuperview() + } + view.addSubview(bgImageView) + bgImageView.snp.makeConstraints { make in + make.top.right.left.equalToSuperview() + make.height.equalTo(981*width) + } + view.addSubview(suggestionsTableView) + suggestionsTableView.snp.makeConstraints { make in + make.top.equalTo(searchView.snp.bottom) + make.left.equalTo(searchView.snp.left).offset(16*width) + make.right.equalTo(searchView.snp.right).offset(-16*width) + make.height.equalTo(240*width) + } + suggestionsTableView.isHidden = true + } + //生成一个顶部搜索框 + private func createSearchView() -> UIView{ + let searchView:UIView = UIView() + searchView.backgroundColor = .init(hex: "#212121") + searchView.isUserInteractionEnabled = true + searchView.layer.masksToBounds = true + searchView.layer.cornerRadius = 16*width + //添加一个icon + let iconImageView = UIImageView(image: .init(named: "B_Seach")) + searchView.addSubview(iconImageView) + iconImageView.snp.makeConstraints { make in + make.height.width.equalTo(16*width) + make.left.equalToSuperview().offset(16*width) + make.centerY.equalToSuperview() + } + //添加textField + searchView.addSubview(searchTextField) + searchTextField.snp.makeConstraints { make in + make.top.bottom.equalToSuperview() + make.left.equalTo(iconImageView.snp.right).offset(8*width) + make.right.equalToSuperview().offset(-16*width) + } + return searchView } - - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ - +} +//MARK: - textField +extension MPPositive_SearchViewController:UITextFieldDelegate { + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let text = (textField.text! as NSString).replacingCharacters(in: range, with: string) + guard text.count <= 30 else { + return false + } + if text.isEmpty { + suggestions = nil + }else { + //触发网络请求 + loadSearchSuggestions(text) + } + return true + } + //避免重复请求 + private func cancelDebounceTimer() { + debounceTimer?.invalidate() + debounceTimer = nil + } + //当用户输入文本通过后进行检索 + private func loadSearchSuggestions(_ text:String) { + cancelDebounceTimer() + //停止输入0.8秒后调用 + debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.8, repeats: false) { [weak self] _ in + self?.fetchSearchSuggestions(text) + } + } + //获取建议组 + private func fetchSearchSuggestions(_ text:String) { + MP_NetWorkManager.shared.requestSearchSuggestions(text) { [weak self] (result) in + self?.suggestions = result + } + } + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + //判断textField是否存在文本 + if let text = textField.text, text.isEmpty != true { + //用户输入了文本 + let showVC = MPPositive_SearchResultShowViewController() + + navigationController?.pushViewController(showVC, animated: true) + return true + }else { + return false + } + } +} +//MARK: - tableView +extension MPPositive_SearchViewController:UITableViewDataSource, UITableViewDelegate { + func numberOfSections(in tableView: UITableView) -> Int { + return suggestions != nil ? suggestions.count:0 + } + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return suggestions != nil ? suggestions[section].count:0 + } + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if suggestions[indexPath.section][indexPath.row].reviewUrls != nil { + let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_SearchSuggestionListTableViewCellID, for: indexPath) as! MPPositive_SearchSuggestionListTableViewCell + cell.item = suggestions[indexPath.section][indexPath.row] + return cell + }else { + let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_SearchSuggestionItemTableViewCellID, for: indexPath) as! MPPositive_SearchSuggestionItemTableViewCell + cell.item = suggestions[indexPath.section][indexPath.row] + return cell + } + } } diff --git a/MusicPlayer/MP/MPPositive/Views/Base/MPPositive_BottomShowView.swift b/MusicPlayer/MP/MPPositive/Views/Base/MPPositive_BottomShowView.swift index 9d586fe..558352f 100644 --- a/MusicPlayer/MP/MPPositive/Views/Base/MPPositive_BottomShowView.swift +++ b/MusicPlayer/MP/MPPositive/Views/Base/MPPositive_BottomShowView.swift @@ -6,6 +6,7 @@ // import UIKit +import Kingfisher ///b面底部播放音乐展示View class MPPositive_BottomShowView: UIView { //绿色背景图片 @@ -37,14 +38,22 @@ class MPPositive_BottomShowView: UIView { btn.addTarget(self, action: #selector(switchStatuClick(_ :)), for: .touchUpInside) return btn }() + //展开音乐播放列表 + var showListBlock:(() -> Void)? override init(frame: CGRect) { super.init(frame: frame) + //添加一个监听 + NotificationCenter.notificationKey.add(observer: self, selector: #selector(currentVideoSwitchAction(_:)), notificationName: .positive_player_reload) + NotificationCenter.notificationKey.add(observer: self, selector: #selector(statusSwitchAction(_:)), notificationName: .switch_player_status) confirgue() } required init?(coder: NSCoder) { super.init(coder: coder) } + deinit { + NotificationCenter.default.removeObserver(self) + } //配置 private func confirgue() { addSubview(bgGreenImageView) @@ -77,13 +86,62 @@ class MPPositive_BottomShowView: UIView { make.right.equalTo(titleLabel) } } - + //切换当前播放音乐时会触发 + @objc private func currentVideoSwitchAction(_ sender:Notification) { + //更新显示的内容 + DispatchQueue.main.async { + [weak self] in + guard let self = self else {return} + if MP_PlayerManager.shared.loadPlayer?.currentVideo != nil { + //存在当前播放音乐,更新显示 + coverImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer?.currentVideo.coverUrl, placeholder: placeholderImage) + titleLabel.text = MP_PlayerManager.shared.loadPlayer?.currentVideo?.title ?? "" + subtitleLabel.text = MP_PlayerManager.shared.loadPlayer?.currentVideo.subtitle ?? "" + } + } + } + //切换播放器状态时 + @objc private func statusSwitchAction(_ sender:Notification) { + if sender.object != nil { + let state:MP_PlayerStateType = sender.object as! MP_PlayerStateType + DispatchQueue.main.async { + [weak self] in + switch state { + case .Playing: + self?.playStatuBtn.isSelected = true + default: + self?.playStatuBtn.isSelected = false + } + } + } + } //展开当前音乐列表 @objc private func expandListsClick(_ sender:UIButton) { - + guard showListBlock != nil else { + return + } + showListBlock!() } //切换播放音乐状态 @objc private func switchStatuClick(_ sender:UIButton) { - + guard MP_PlayerManager.shared.loadPlayer != nil else { + return + } + //在当前音乐填充好之前,禁止触发点击 + switch MP_PlayerManager.shared.getPlayState() { + case .Null: + //启动播放 + MP_PlayerManager.shared.play() + case .Playing: + //播放中,进入暂停 + MP_PlayerManager.shared.pause { + [weak self] in + } + case .Pause: + //暂停中,进入继续 + MP_PlayerManager.shared.resume { + [weak self] in + } + } } } diff --git a/MusicPlayer/MP/MPPositive/Views/Player/MPPositive_PlayerListShowTableViewCell.swift b/MusicPlayer/MP/MPPositive/Views/Player/MPPositive_PlayerListShowTableViewCell.swift index efe3d0c..4a250ad 100644 --- a/MusicPlayer/MP/MPPositive/Views/Player/MPPositive_PlayerListShowTableViewCell.swift +++ b/MusicPlayer/MP/MPPositive/Views/Player/MPPositive_PlayerListShowTableViewCell.swift @@ -29,6 +29,14 @@ class MPPositive_PlayerListShowTableViewCell: UITableViewCell { }() var song:MPPositive_SongItemModel!{ didSet{ + //判断是否当前播放 + if song.videoId == MP_PlayerManager.shared.loadPlayer.currentVideo.song.videoId { + titleLabel.textColor = .init(hex: "#80F988") + subtitleLabel.textColor = .init(hex: "#80F988") + }else { + titleLabel.textColor = .white + subtitleLabel.textColor = .init(hex: "#FFFFFF", alpha: 0.6) + } coverImageView.kf.setImage(with: URL(string: song.reviewUrls?.first ?? ""), placeholder: placeholderImage) titleLabel.text = song.title subtitleLabel.text = song.shortBylineText diff --git a/MusicPlayer/MP/MPPositive/Views/Player/MPPositive_PlayerLyricView.swift b/MusicPlayer/MP/MPPositive/Views/Player/MPPositive_PlayerLyricView.swift index 74c32a6..2359c0e 100644 --- a/MusicPlayer/MP/MPPositive/Views/Player/MPPositive_PlayerLyricView.swift +++ b/MusicPlayer/MP/MPPositive/Views/Player/MPPositive_PlayerLyricView.swift @@ -21,8 +21,8 @@ class MPPositive_PlayerLyricView: UIView { scrollView.addSubview(lyricsLabel) scrollView.contentSize = .init(width: screen_Width, height: lyricsLabel.frame.origin.y + lyricsLabel.frame.height) lyricsLabel.snp.makeConstraints { make in - make.left.equalToSuperview().offset(20*width) - make.right.equalToSuperview().offset(-20*width) + make.width.equalToSuperview().multipliedBy(0.89) + make.centerX.equalToSuperview() make.top.equalToSuperview().offset(15*width) make.bottom.equalToSuperview().offset(30*width) } diff --git a/MusicPlayer/MP/MPPositive/Views/Search/MPPositive_SearchSuggestionItemTableViewCell.swift b/MusicPlayer/MP/MPPositive/Views/Search/MPPositive_SearchSuggestionItemTableViewCell.swift new file mode 100644 index 0000000..1735363 --- /dev/null +++ b/MusicPlayer/MP/MPPositive/Views/Search/MPPositive_SearchSuggestionItemTableViewCell.swift @@ -0,0 +1,52 @@ +// +// MPPositive_SearchSuggestionItemTableViewCell.swift +// MusicPlayer +// +// Created by Mr.Zhou on 2024/5/12. +// + +import UIKit + +class MPPositive_SearchSuggestionItemTableViewCell: UITableViewCell { + //搜索Icon + private lazy var iconImageView:UIImageView = UIImageView(image: .init(named: "B_Seach")) + private lazy var titleLabel:UILabel = createLabel(font: .systemFont(ofSize: 12*width, weight: .regular), textColor: .init(hex: "#FFFFFF", alpha: 0.6), textAlignment: .left) + var item:MPPositive_SearchSuggestionItemModel!{ + didSet{ + titleLabel.text = item.title ?? "" + } + } + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + backgroundColor = .clear + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + override func awakeFromNib() { + super.awakeFromNib() + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + private func configure() { + contentView.addSubview(iconImageView) + iconImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(12*width) + make.width.height.equalTo(30*width) + make.centerY.equalToSuperview() + } + contentView.addSubview(titleLabel) + titleLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalTo(iconImageView.snp.right).offset(10*width) + make.right.equalToSuperview().offset(-12*width) + } + } +} diff --git a/MusicPlayer/MP/MPPositive/Views/Search/MPPositive_SearchSuggestionListTableViewCell.swift b/MusicPlayer/MP/MPPositive/Views/Search/MPPositive_SearchSuggestionListTableViewCell.swift new file mode 100644 index 0000000..15e05cc --- /dev/null +++ b/MusicPlayer/MP/MPPositive/Views/Search/MPPositive_SearchSuggestionListTableViewCell.swift @@ -0,0 +1,65 @@ +// +// MPPositive_SearchSuggestionListTableViewCell.swift +// MusicPlayer +// +// Created by Mr.Zhou on 2024/5/12. +// + +import UIKit +import Kingfisher +class MPPositive_SearchSuggestionListTableViewCell: UITableViewCell { + //搜索Icon + private lazy var reviewImageView:UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + return imageView + }() + private lazy var titleLabel:UILabel = createLabel(font: .systemFont(ofSize: 12*width, weight: .regular), textColor: .white, textAlignment: .left) + private lazy var subtitleLabel:UILabel = createLabel(font: .systemFont(ofSize: 12*width, weight: .regular), textColor: .init(hex: "#FFFFFF", alpha: 0.6), textAlignment: .left) + var item:MPPositive_SearchSuggestionItemModel!{ + didSet{ + reviewImageView.kf.setImage(with: URL(string: item.reviewUrls?.last ?? ""), placeholder: placeholderImage) + titleLabel.text = item.title ?? "" + subtitleLabel.text = item.subtitle ?? "" + } + } + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + backgroundColor = .clear + configure() + } + required init?(coder: NSCoder) { + super.init(coder: coder) + } + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + private func configure() { + contentView.addSubview(reviewImageView) + reviewImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(12*width) + make.width.height.equalTo(30*width) + make.centerY.equalToSuperview() + } + contentView.addSubview(titleLabel) + titleLabel.snp.makeConstraints { make in + make.top.equalTo(reviewImageView.snp.top) + make.left.equalTo(reviewImageView.snp.right).offset(10*width) + make.right.equalToSuperview().offset(-12*width) + } + contentView.addSubview(subtitleLabel) + subtitleLabel.snp.makeConstraints { make in + make.bottom.equalTo(reviewImageView.snp.bottom) + make.left.equalTo(titleLabel.snp.left) + make.right.equalTo(titleLabel.snp.right) + } + } +}