├── .gitignore ├── LICENSE ├── NeteaseTVDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ ├── fengyn.xcuserdatad │ │ ├── Bookmarks │ │ │ └── bookmarks.plist │ │ └── UserInterfaceState.xcuserstate │ │ └── zhangdong.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── NeteaseTVDemo.xcscheme └── xcuserdata │ ├── fengyn.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ └── xcschememanagement.plist │ └── zhangdong.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── NeteaseTVDemo ├── AppDelegate.swift ├── Assets.xcassets │ ├── #707AFA.colorset │ │ └── Contents.json │ ├── #AE1010.colorset │ │ └── Contents.json │ ├── #FF8354.colorset │ │ └── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── App Icon & Top Shelf Image.brandassets │ │ ├── App Icon - App Store.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── newicon_back 1.png │ │ │ │ │ └── newicon_back 2.png │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── newicon_front 1.png │ │ │ │ │ └── newicon_front 2.png │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── newicon_middle 1.png │ │ │ │ └── newicon_middle 2.png │ │ │ │ └── Contents.json │ │ ├── App Icon.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── newicon_back 1.png │ │ │ │ │ ├── newicon_back 2.png │ │ │ │ │ ├── newicon_back 3.png │ │ │ │ │ └── newicon_back.png │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── newicon_front 1.png │ │ │ │ │ ├── newicon_front 2.png │ │ │ │ │ ├── newicon_front 3.png │ │ │ │ │ └── newicon_front.png │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── newicon_middle 1.png │ │ │ │ ├── newicon_middle 2.png │ │ │ │ ├── newicon_middle 3.png │ │ │ │ └── newicon_middle.png │ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Top Shelf Image Wide.imageset │ │ │ ├── Contents.json │ │ │ └── newicon_back (2).png │ │ └── Top Shelf Image.imageset │ │ │ ├── Contents.json │ │ │ └── newicon_back (3).png │ ├── Contents.json │ ├── Cursor.imageset │ │ ├── Contents.json │ │ └── mac-osx-arrow-cursor.png │ ├── Pointer.imageset │ │ ├── Contents.json │ │ └── mac-osx-pointer-cursor.png │ ├── add_account.imageset │ │ ├── Contents.json │ │ ├── add_account.png │ │ ├── add_account@2x.png │ │ └── add_account@3x.png │ ├── bgColor.colorset │ │ └── Contents.json │ ├── bgImage.imageset │ │ ├── Contents.json │ │ ├── newicon_back.png │ │ ├── newicon_back@2x.png │ │ └── newicon_back@3x.png │ ├── gradient_bg.imageset │ │ ├── Contents.json │ │ ├── Rectangle 2.png │ │ ├── Rectangle 2@2x.png │ │ ├── Rectangle 2@3x.png │ │ ├── gradient_bg 1.png │ │ ├── gradient_bg.png │ │ ├── gradient_bg@2x 1.png │ │ ├── gradient_bg@2x.png │ │ ├── gradient_bg@3x 1.png │ │ └── gradient_bg@3x.png │ ├── grayColor.colorset │ │ └── Contents.json │ ├── sectionTitleColor.colorset │ │ └── Contents.json │ └── titleColor.colorset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Extensions │ ├── Int+Extension.swift │ ├── String+Extension.swift │ ├── UIViewController+Extension.swift │ └── UserDefault+.swift ├── Info.plist ├── Modules │ ├── Browser │ │ ├── MenuIcons │ │ │ ├── go-back-left-arrow.png │ │ │ ├── house-outline.png │ │ │ ├── maximize-2.png │ │ │ ├── menu-2.png │ │ │ ├── menu-button.png │ │ │ ├── refresh-button.png │ │ │ ├── resize-arrows.png │ │ │ └── right-arrow-forward.png │ │ ├── NeteaseTVDemo-Bridging-Header.h │ │ ├── WKBrowserViewController.h │ │ └── WKBrowserViewController.m │ ├── Detail │ │ ├── WKAlbumDetailViewController.swift │ │ ├── WKDescView.swift │ │ ├── WKDescViewController.swift │ │ ├── WKMoreView.swift │ │ ├── WKPlayListDetailViewController.swift │ │ ├── WKSingerDetailAlbumListVC.swift │ │ ├── WKSingerDetailViewController.swift │ │ ├── WKSingerMVListVC.swift │ │ ├── WKSongListViewController.swift │ │ └── WKUserCollectionViewCell.swift │ ├── Find │ │ ├── WKFindModel.swift │ │ └── WKFindViewController.swift │ ├── Login │ │ ├── WKLoginViewController.swift │ │ └── WKUserModel.swift │ ├── Player │ │ ├── CustomAudioModel.swift │ │ ├── WKCountdown.swift │ │ ├── WKPlayer.swift │ │ ├── WKPlayerError.swift │ │ ├── WKPlayerSettings.swift │ │ ├── WKPlayerStateModel.swift │ │ └── WKPlayerTool.swift │ ├── Playing │ │ ├── ViewController.swift │ │ ├── WKCommentTableViewCell.swift │ │ ├── WKCommentViewController.swift │ │ ├── WKLyricProgressLabel.swift │ │ ├── WKLyricTableViewCell.swift │ │ ├── WKPlayingViewController.swift │ │ └── WKSlider.swift │ ├── Podcast │ │ ├── WKPodcastDetailViewController.swift │ │ └── WKPodcastViewController.swift │ ├── Profile │ │ ├── WKAboutViewController.swift │ │ ├── WKAccountCell.swift │ │ ├── WKAvatarView.swift │ │ ├── WKMyCloudSongVC.swift │ │ ├── WKMyCollectionAlbumVC.swift │ │ ├── WKMyCollectionMVVC.swift │ │ ├── WKMyCollectionPlaylistVC.swift │ │ ├── WKMyCollectionPodcastVC.swift │ │ ├── WKMyCollectionVC.swift │ │ ├── WKMyPlaylistViewController.swift │ │ ├── WKProfileHeader.swift │ │ ├── WKProfileViewController.swift │ │ ├── WKRecentAlbumListVC.swift │ │ ├── WKRecentPlayViewController.swift │ │ ├── WKRecentPodcastListVC.swift │ │ ├── WKRecentSongListVC.swift │ │ ├── WKRecentVideoVC.swift │ │ ├── WKRencentPlaylistVC.swift │ │ ├── WKRencentVoiceListVC.swift │ │ ├── WKSettingViewController.swift │ │ ├── WKSongCollectionVC.swift │ │ └── WKSwitchAccountVC.swift │ ├── Recommend │ │ ├── Banner │ │ │ ├── FSPageControl.swift │ │ │ ├── FSPageViewLayout.swift │ │ │ ├── FSPageViewTransformer.swift │ │ │ ├── FSPagerCollectionView.swift │ │ │ ├── FSPagerView.swift │ │ │ ├── FSPagerViewCell.swift │ │ │ ├── FSPagerViewLayoutAttributes.swift │ │ │ ├── FSPagerViewObjcCompat.h │ │ │ └── FSPagerViewObjcCompat.m │ │ ├── WKDailySongsViewController.swift │ │ ├── WKRecommendPlaylistVC.swift │ │ ├── WKRecommendViewController.swift │ │ ├── WKSingleSongView.swift │ │ └── WKSongCollectionViewCell.swift │ ├── RemoteControl │ │ ├── WKNowPlayingInfo.swift │ │ └── WKRemoteCommandManager.swift │ ├── Search │ │ ├── WKSearchResultViewController.swift │ │ ├── WKSearchTypeModel.swift │ │ └── WKSearchViewController.swift │ └── Video │ │ ├── WKMVViewController.swift │ │ └── WKVideoViewController.swift ├── NeteaseTVDemo.entitlements ├── Settings.swift ├── WKConfig.swift ├── WKInputViewController.swift ├── WKMVCollectionViewCell.swift ├── WKPlayListCollectionViewCell.swift ├── WKPlayListTableViewCell.swift ├── WKSegmentCell.swift ├── WKSongTableViewCell.swift └── WKTabBarViewController.swift ├── README.md ├── TopShelf ├── ContentProvider.swift ├── Info.plist └── TopShelf.entitlements └── images ├── WechatIMG95.png └── preview.png /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Icon must end with two \r 7 | Icon 8 | 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | .com.apple.timemachine.donotpresent 21 | 22 | # Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | 30 | # Xcode 31 | # 32 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 33 | 34 | ## User settings 35 | xcuserdata/ 36 | 37 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 38 | *.xcscmblueprint 39 | *.xccheckout 40 | 41 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 42 | build/ 43 | DerivedData/ 44 | *.moved-aside 45 | *.pbxuser 46 | !default.pbxuser 47 | *.mode1v3 48 | !default.mode1v3 49 | *.mode2v3 50 | !default.mode2v3 51 | *.perspectivev3 52 | !default.perspectivev3 53 | 54 | ## Gcc Patch 55 | /*.gcno 56 | 57 | 58 | # Xcode 59 | # 60 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 61 | 62 | ## User settings 63 | xcuserdata/ 64 | 65 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 66 | *.xcscmblueprint 67 | *.xccheckout 68 | 69 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 70 | build/ 71 | DerivedData/ 72 | *.moved-aside 73 | *.pbxuser 74 | !default.pbxuser 75 | *.mode1v3 76 | !default.mode1v3 77 | *.mode2v3 78 | !default.mode2v3 79 | *.perspectivev3 80 | !default.perspectivev3 81 | 82 | ## Obj-C/Swift specific 83 | *.hmap 84 | 85 | ## App packaging 86 | *.ipa 87 | *.dSYM.zip 88 | *.dSYM 89 | 90 | ## Playgrounds 91 | timeline.xctimeline 92 | playground.xcworkspace 93 | 94 | # Swift Package Manager 95 | # 96 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 97 | # Packages/ 98 | # Package.pins 99 | # Package.resolved 100 | # *.xcodeproj 101 | # 102 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 103 | # hence it is not needed unless you have added a package configuration file to your project 104 | # .swiftpm 105 | 106 | .build/ 107 | 108 | # CocoaPods 109 | # 110 | # We recommend against adding the Pods directory to your .gitignore. However 111 | # you should judge for yourself, the pros and cons are mentioned at: 112 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 113 | # 114 | # Pods/ 115 | # 116 | # Add this line if you want to avoid checking in source code from the Xcode workspace 117 | # *.xcworkspace 118 | 119 | # Carthage 120 | # 121 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 122 | # Carthage/Checkouts 123 | 124 | Carthage/Build/ 125 | 126 | # Accio dependency management 127 | Dependencies/ 128 | .accio/ 129 | 130 | # fastlane 131 | # 132 | # It is recommended to not store the screenshots in the git repo. 133 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 134 | # For more information about the recommended setup visit: 135 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 136 | 137 | fastlane/report.xml 138 | fastlane/Preview.html 139 | fastlane/screenshots/**/*.png 140 | fastlane/test_output 141 | 142 | # Code Injection 143 | # 144 | # After new code Injection tools there's a generated folder /iOSInjectionProject 145 | # https://github.com/johnno1962/injectionforxcode 146 | 147 | iOSInjectionProject/ 148 | .idea 149 | -------------------------------------------------------------------------------- /NeteaseTVDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NeteaseTVDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NeteaseTVDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "alamofire", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/Alamofire/Alamofire.git", 7 | "state" : { 8 | "revision" : "b2fa556e4e48cbf06cf8c63def138c98f4b811fa", 9 | "version" : "5.8.0" 10 | } 11 | }, 12 | { 13 | "identity" : "colorfulx", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/Lakr233/ColorfulX.git", 16 | "state" : { 17 | "revision" : "24bbc2f4b27c853f2be5ebed144252d9bcd27ee0", 18 | "version" : "2.4.5" 19 | } 20 | }, 21 | { 22 | "identity" : "efqrcode", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/EFPrefix/EFQRCode.git", 25 | "state" : { 26 | "branch" : "main", 27 | "revision" : "5cc6cc0b03742b23d1958642210fbf23a20dac53" 28 | } 29 | }, 30 | { 31 | "identity" : "kingfisher", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/onevcat/Kingfisher.git", 34 | "state" : { 35 | "revision" : "b6f62758f21a8c03cd64f4009c037cfa580a256e", 36 | "version" : "7.9.1" 37 | } 38 | }, 39 | { 40 | "identity" : "marqueelabel", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/cbpowell/MarqueeLabel", 43 | "state" : { 44 | "branch" : "master", 45 | "revision" : "bc00d4cbff7f6c416035e3d16c21885452cd159e" 46 | } 47 | }, 48 | { 49 | "identity" : "neteaserequest", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/ZhangDo/NeteaseRequest.git", 52 | "state" : { 53 | "revision" : "230979faa866ea1675063e5f2748fe8b481966da", 54 | "version" : "1.2.3" 55 | } 56 | }, 57 | { 58 | "identity" : "snapkit", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/SnapKit/SnapKit.git", 61 | "state" : { 62 | "revision" : "f222cbdf325885926566172f6f5f06af95473158", 63 | "version" : "5.6.0" 64 | } 65 | }, 66 | { 67 | "identity" : "springinterpolation", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/Lakr233/SpringInterpolation.git", 70 | "state" : { 71 | "revision" : "be8721cffc87ec514fa13ba5dab586a2730f51c9", 72 | "version" : "1.1.2" 73 | } 74 | }, 75 | { 76 | "identity" : "swift_qrcodejs", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/ApolloZhu/swift_qrcodejs.git", 79 | "state" : { 80 | "revision" : "374dc7f7b9e76c6aeb393f6a84590c6d387e1ecb", 81 | "version" : "2.2.2" 82 | } 83 | }, 84 | { 85 | "identity" : "swiftyjson", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/SwiftyJSON/SwiftyJSON.git", 88 | "state" : { 89 | "revision" : "2b6054efa051565954e1d2b9da831680026cd768", 90 | "version" : "4.3.0" 91 | } 92 | } 93 | ], 94 | "version" : 2 95 | } 96 | -------------------------------------------------------------------------------- /NeteaseTVDemo.xcodeproj/project.xcworkspace/xcuserdata/fengyn.xcuserdatad/Bookmarks/bookmarks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | top-level-items 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NeteaseTVDemo.xcodeproj/project.xcworkspace/xcuserdata/fengyn.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo.xcodeproj/project.xcworkspace/xcuserdata/fengyn.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /NeteaseTVDemo.xcodeproj/project.xcworkspace/xcuserdata/zhangdong.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo.xcodeproj/project.xcworkspace/xcuserdata/zhangdong.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /NeteaseTVDemo.xcodeproj/xcshareddata/xcschemes/NeteaseTVDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 57 | 58 | 59 | 60 | 66 | 68 | 74 | 75 | 76 | 77 | 79 | 80 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /NeteaseTVDemo.xcodeproj/xcuserdata/fengyn.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /NeteaseTVDemo.xcodeproj/xcuserdata/zhangdong.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Basic B&W QR Code (Playground) 1.xcscheme 8 | 9 | isShown 10 | 11 | orderHint 12 | 14 13 | 14 | Basic B&W QR Code (Playground) 2.xcscheme 15 | 16 | isShown 17 | 18 | orderHint 19 | 15 20 | 21 | Basic B&W QR Code (Playground).xcscheme 22 | 23 | isShown 24 | 25 | orderHint 26 | 13 27 | 28 | European SEPA Money Transfer + Watermark (Playground) 1.xcscheme 29 | 30 | isShown 31 | 32 | orderHint 33 | 17 34 | 35 | European SEPA Money Transfer + Watermark (Playground) 2.xcscheme 36 | 37 | isShown 38 | 39 | orderHint 40 | 18 41 | 42 | European SEPA Money Transfer + Watermark (Playground).xcscheme 43 | 44 | isShown 45 | 46 | orderHint 47 | 16 48 | 49 | NeteaseTVDemo.xcscheme_^#shared#^_ 50 | 51 | orderHint 52 | 0 53 | 54 | Playground (Playground) 1.xcscheme 55 | 56 | isShown 57 | 58 | orderHint 59 | 11 60 | 61 | Playground (Playground) 2.xcscheme 62 | 63 | isShown 64 | 65 | orderHint 66 | 12 67 | 68 | Playground (Playground).xcscheme 69 | 70 | isShown 71 | 72 | orderHint 73 | 10 74 | 75 | README (Playground) 1.xcscheme 76 | 77 | isShown 78 | 79 | orderHint 80 | 8 81 | 82 | README (Playground) 2.xcscheme 83 | 84 | isShown 85 | 86 | orderHint 87 | 9 88 | 89 | README (Playground).xcscheme 90 | 91 | isShown 92 | 93 | orderHint 94 | 7 95 | 96 | SnapKitPlayground (Playground) 1.xcscheme 97 | 98 | isShown 99 | 100 | orderHint 101 | 3 102 | 103 | SnapKitPlayground (Playground) 2.xcscheme 104 | 105 | isShown 106 | 107 | orderHint 108 | 4 109 | 110 | SnapKitPlayground (Playground).xcscheme 111 | 112 | isShown 113 | 114 | orderHint 115 | 0 116 | 117 | TopShelf.xcscheme_^#shared#^_ 118 | 119 | orderHint 120 | 1 121 | 122 | User Guide (Playground) 1.xcscheme 123 | 124 | isShown 125 | 126 | orderHint 127 | 5 128 | 129 | User Guide (Playground) 2.xcscheme 130 | 131 | isShown 132 | 133 | orderHint 134 | 6 135 | 136 | User Guide (Playground).xcscheme 137 | 138 | isShown 139 | 140 | orderHint 141 | 2 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/#707AFA.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFA", 9 | "green" : "0x7A", 10 | "red" : "0x70" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/#AE1010.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x10", 9 | "green" : "0x10", 10 | "red" : "0xAE" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/#FF8354.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x54", 9 | "green" : "0x83", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "newicon_back 1.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "newicon_back 2.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/newicon_back 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/newicon_back 1.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/newicon_back 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/newicon_back 2.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "newicon_front 1.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "newicon_front 2.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/newicon_front 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/newicon_front 1.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/newicon_front 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/newicon_front 2.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "newicon_middle 1.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "newicon_middle 2.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/newicon_middle 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/newicon_middle 1.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/newicon_middle 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/newicon_middle 2.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "newicon_back 1.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "newicon_back.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "newicon_back 2.png", 15 | "idiom" : "tv-marketing", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "filename" : "newicon_back 3.png", 20 | "idiom" : "tv-marketing", 21 | "scale" : "2x" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/newicon_back 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/newicon_back 1.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/newicon_back 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/newicon_back 2.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/newicon_back 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/newicon_back 3.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/newicon_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/newicon_back.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "newicon_front 1.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "newicon_front.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "newicon_front 2.png", 15 | "idiom" : "tv-marketing", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "filename" : "newicon_front 3.png", 20 | "idiom" : "tv-marketing", 21 | "scale" : "2x" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/newicon_front 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/newicon_front 1.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/newicon_front 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/newicon_front 2.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/newicon_front 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/newicon_front 3.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/newicon_front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/newicon_front.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "newicon_middle 1.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "newicon_middle.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "newicon_middle 2.png", 15 | "idiom" : "tv-marketing", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "filename" : "newicon_middle 3.png", 20 | "idiom" : "tv-marketing", 21 | "scale" : "2x" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/newicon_middle 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/newicon_middle 1.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/newicon_middle 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/newicon_middle 2.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/newicon_middle 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/newicon_middle 3.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/newicon_middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/newicon_middle.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "newicon_back (2).png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "tv-marketing", 14 | "scale" : "1x" 15 | }, 16 | { 17 | "idiom" : "tv-marketing", 18 | "scale" : "2x" 19 | } 20 | ], 21 | "info" : { 22 | "author" : "xcode", 23 | "version" : 1 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/newicon_back (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/newicon_back (2).png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "newicon_back (3).png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "tv-marketing", 14 | "scale" : "1x" 15 | }, 16 | { 17 | "idiom" : "tv-marketing", 18 | "scale" : "2x" 19 | } 20 | ], 21 | "info" : { 22 | "author" : "xcode", 23 | "version" : 1 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/newicon_back (3).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/newicon_back (3).png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/Cursor.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mac-osx-arrow-cursor.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/Cursor.imageset/mac-osx-arrow-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/Cursor.imageset/mac-osx-arrow-cursor.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/Pointer.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mac-osx-pointer-cursor.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/Pointer.imageset/mac-osx-pointer-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/Pointer.imageset/mac-osx-pointer-cursor.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/add_account.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "add_account.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "add_account@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "add_account@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/add_account.imageset/add_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/add_account.imageset/add_account.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/add_account.imageset/add_account@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/add_account.imageset/add_account@2x.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/add_account.imageset/add_account@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/add_account.imageset/add_account@3x.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/bgColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "tvos", 6 | "reference" : "tintColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.223", 22 | "green" : "0.223", 23 | "red" : "0.223" 24 | } 25 | }, 26 | "idiom" : "universal" 27 | } 28 | ], 29 | "info" : { 30 | "author" : "xcode", 31 | "version" : 1 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/bgImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "newicon_back.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "newicon_back@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "newicon_back@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/bgImage.imageset/newicon_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/bgImage.imageset/newicon_back.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/bgImage.imageset/newicon_back@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/bgImage.imageset/newicon_back@2x.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/bgImage.imageset/newicon_back@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/bgImage.imageset/newicon_back@3x.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "gradient_bg.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "light" 13 | } 14 | ], 15 | "filename" : "Rectangle 2.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "dark" 24 | } 25 | ], 26 | "filename" : "gradient_bg 1.png", 27 | "idiom" : "universal", 28 | "scale" : "1x" 29 | }, 30 | { 31 | "filename" : "gradient_bg@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "appearances" : [ 37 | { 38 | "appearance" : "luminosity", 39 | "value" : "light" 40 | } 41 | ], 42 | "filename" : "Rectangle 2@2x.png", 43 | "idiom" : "universal", 44 | "scale" : "2x" 45 | }, 46 | { 47 | "appearances" : [ 48 | { 49 | "appearance" : "luminosity", 50 | "value" : "dark" 51 | } 52 | ], 53 | "filename" : "gradient_bg@2x 1.png", 54 | "idiom" : "universal", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "filename" : "gradient_bg@3x.png", 59 | "idiom" : "universal", 60 | "scale" : "3x" 61 | }, 62 | { 63 | "appearances" : [ 64 | { 65 | "appearance" : "luminosity", 66 | "value" : "light" 67 | } 68 | ], 69 | "filename" : "Rectangle 2@3x.png", 70 | "idiom" : "universal", 71 | "scale" : "3x" 72 | }, 73 | { 74 | "appearances" : [ 75 | { 76 | "appearance" : "luminosity", 77 | "value" : "dark" 78 | } 79 | ], 80 | "filename" : "gradient_bg@3x 1.png", 81 | "idiom" : "universal", 82 | "scale" : "3x" 83 | } 84 | ], 85 | "info" : { 86 | "author" : "xcode", 87 | "version" : 1 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/Rectangle 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/Rectangle 2.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/Rectangle 2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/Rectangle 2@2x.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/Rectangle 2@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/Rectangle 2@3x.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/gradient_bg 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/gradient_bg 1.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/gradient_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/gradient_bg.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/gradient_bg@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/gradient_bg@2x 1.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/gradient_bg@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/gradient_bg@2x.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/gradient_bg@3x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/gradient_bg@3x 1.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/gradient_bg@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Assets.xcassets/gradient_bg.imageset/gradient_bg@3x.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/grayColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-gray", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0xE2" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/sectionTitleColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-srgb", 6 | "components" : { 7 | "alpha" : "0.892", 8 | "blue" : "0.080", 9 | "green" : "0.086", 10 | "red" : "0.034" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "extended-srgb", 24 | "components" : { 25 | "alpha" : "0.801", 26 | "blue" : "0.995", 27 | "green" : "0.994", 28 | "red" : "0.994" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Assets.xcassets/titleColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "tvos", 6 | "reference" : "labelColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "platform" : "tvos", 19 | "reference" : "labelColor" 20 | }, 21 | "idiom" : "universal" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Extensions/Int+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | extension Int { 5 | // MARK: 转换万单位 6 | func toTenThousandString(scale: Int = 1) -> String { 7 | if self < 0 { 8 | return "0" 9 | } else if self <= 9999 { 10 | return "\(self)" 11 | } else { 12 | let doub = CGFloat(self) / 10000 13 | let str = String(format: "%.\(scale)f", doub) 14 | let start_index = str.index(str.endIndex, offsetBy: -1) 15 | let suffix = String(str[start_index ..< str.endIndex]) 16 | if suffix == "0" { 17 | let toIndex = str.index(str.endIndex, offsetBy: -2) 18 | return String(str[str.startIndex ..< toIndex]) + "万" 19 | } else { 20 | return str + "万" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Extensions/String+Extension.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | 5 | extension String { 6 | ///歌词解析 7 | func parserLyric() -> (times: [String], words: [String]) { 8 | var lineLyrics: [String] = [] 9 | var words: [String] = [] 10 | var times: [String] = [] 11 | let seps = self.components(separatedBy: "[") 12 | for lineLyric in seps { 13 | if seps.count > 0 { 14 | lineLyrics = lineLyric.components(separatedBy: "]") 15 | if lineLyric != "\n" { 16 | times.append(lineLyrics.first ?? "0") 17 | words.append(lineLyrics.count > 1 ? lineLyrics[1] : "") 18 | } 19 | } 20 | } 21 | return (times, words) 22 | } 23 | 24 | ///播放时间条转换成秒的格式 25 | func convertToSeconds() -> UInt { 26 | let components = self.split(separator: ":") 27 | guard components.count == 2 else { return 0 } 28 | 29 | let minuteSeconds = Double(components[0])! * 60 30 | let seconds = Double(components[1])! 31 | 32 | return UInt(round(minuteSeconds + seconds)) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Extensions/UIViewController+Extension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import ColorfulX 3 | extension UIViewController { 4 | func topMostViewController() -> UIViewController { 5 | if let presented = presentedViewController { 6 | return presented.topMostViewController() 7 | } 8 | 9 | if let navigation = self as? UINavigationController { 10 | return navigation.visibleViewController?.topMostViewController() ?? navigation 11 | } 12 | 13 | if let tab = self as? UITabBarController { 14 | return tab.selectedViewController?.topMostViewController() ?? tab 15 | } 16 | 17 | return self 18 | } 19 | 20 | static func topMostViewController() -> UIViewController { 21 | return AppDelegate.shared.window!.rootViewController!.topMostViewController() 22 | } 23 | 24 | func getDefalutColors() -> [RGBColor] { 25 | // [make(254, 116, 97), make(243, 8, 32), make(250, 193, 208), make(193, 123, 126)] 26 | // [make(22, 4, 74), make(240, 54, 248), make(79, 216, 248), make(74, 0, 217)] 27 | let colors = [UIColor(red: 22.0 / 255.0, green: 4.0 / 255.0, blue: 74.0 / 255.0, alpha: 1.0), 28 | UIColor(red: 240.0 / 255.0, green: 54.0 / 255.0, blue: 248.0 / 255.0, alpha: 1.0), 29 | UIColor(red: 79.0 / 255.0, green: 216.0 / 255.0, blue: 248.0 / 255.0, alpha: 1.0), 30 | UIColor(red: 74.0 / 255.0, green: 0.0 / 255.0, blue: 217.0 / 255.0, alpha: 1.0)] 31 | var rgbColors: [RGBColor] = [] 32 | for color in colors { 33 | rgbColors.append(RGBColor(color)) 34 | } 35 | return rgbColors 36 | } 37 | 38 | func showAlert(_ message: String) { 39 | DispatchQueue.main.async { 40 | let alert = UIAlertController.init(title: "提示", message: message, preferredStyle: .alert) 41 | let confirm = UIAlertAction.init(title: "确定", style: .default, handler: nil) 42 | alert.addAction(confirm) 43 | self.present(alert, animated: true, completion: nil) 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Extensions/UserDefault+.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @propertyWrapper 4 | struct UserDefault { 5 | private let key: String 6 | private let defaultValue: T 7 | private let userDefaults: UserDefaults 8 | 9 | init(_ key: String, defaultValue: T, userDefaults: UserDefaults = .standard) { 10 | self.key = key 11 | self.defaultValue = defaultValue 12 | self.userDefaults = userDefaults 13 | } 14 | 15 | var wrappedValue: T { 16 | get { 17 | guard let value = userDefaults.object(forKey: key) else { 18 | return defaultValue 19 | } 20 | 21 | return value as? T ?? defaultValue 22 | } 23 | set { 24 | if let value = newValue as? OptionalProtocol, value.isNil() { 25 | userDefaults.removeObject(forKey: key) 26 | } else { 27 | userDefaults.set(newValue, forKey: key) 28 | } 29 | } 30 | } 31 | } 32 | 33 | @propertyWrapper 34 | struct UserDefaultCodable { 35 | private let key: String 36 | private let defaultValue: T 37 | private let userDefaults: UserDefaults 38 | 39 | init(_ key: String, defaultValue: T, userDefaults: UserDefaults = .standard) { 40 | self.key = key 41 | self.defaultValue = defaultValue 42 | self.userDefaults = userDefaults 43 | } 44 | 45 | var wrappedValue: T { 46 | get { 47 | guard let value: T? = userDefaults.codable(forKey: key) else { 48 | return defaultValue 49 | } 50 | 51 | return value ?? defaultValue 52 | } 53 | set { 54 | if let value = newValue as? OptionalProtocol, value.isNil() { 55 | userDefaults.removeObject(forKey: key) 56 | } else { 57 | userDefaults.set(codable: newValue, forKey: key) 58 | } 59 | } 60 | } 61 | } 62 | 63 | private protocol OptionalProtocol { 64 | func isNil() -> Bool 65 | } 66 | 67 | extension Optional: OptionalProtocol { 68 | func isNil() -> Bool { 69 | return self == nil 70 | } 71 | } 72 | 73 | extension UserDefaults { 74 | 75 | func set(codable: Element, forKey key: String) { 76 | let data = try? JSONEncoder().encode(codable) 77 | UserDefaults.standard.setValue(data, forKey: key) 78 | } 79 | 80 | func codable(forKey key: String) -> Element? { 81 | guard let data = UserDefaults.standard.data(forKey: key) else { return nil } 82 | let element = try? JSONDecoder().decode(Element.self, from: data) 83 | return element 84 | } 85 | 86 | func setShareValue(codable: Element, forKey key: String) { 87 | if let jsonData = try? JSONEncoder().encode(codable) { 88 | if let jsonString = String(data: jsonData, encoding: .utf8) { 89 | UserDefaults.init(suiteName: "group.com.wk.Vibefy")?.setValue(jsonString, forKey: key) 90 | } 91 | } 92 | } 93 | 94 | func shareListValue(forKey key: String) ->[Element]? { 95 | guard let jsonString = UserDefaults.init(suiteName: "group.com.wk.Vibefy")?.value(forKey: key) as? String else { return nil } 96 | guard let jsonData = jsonString.data(using: .utf8) else { return nil } 97 | let element = try? JSONDecoder().decode([Element].self, from: jsonData) 98 | return element 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleURLSchemes 9 | 10 | vibefy 11 | 12 | 13 | 14 | ITSAppUsesNonExemptEncryption 15 | 16 | NSAppTransportSecurity 17 | 18 | NSAllowsArbitraryLoads 19 | 20 | 21 | UIBackgroundModes 22 | 23 | audio 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Browser/MenuIcons/go-back-left-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Modules/Browser/MenuIcons/go-back-left-arrow.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Browser/MenuIcons/house-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Modules/Browser/MenuIcons/house-outline.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Browser/MenuIcons/maximize-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Modules/Browser/MenuIcons/maximize-2.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Browser/MenuIcons/menu-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Modules/Browser/MenuIcons/menu-2.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Browser/MenuIcons/menu-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Modules/Browser/MenuIcons/menu-button.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Browser/MenuIcons/refresh-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Modules/Browser/MenuIcons/refresh-button.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Browser/MenuIcons/resize-arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Modules/Browser/MenuIcons/resize-arrows.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Browser/MenuIcons/right-arrow-forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/NeteaseTVDemo/Modules/Browser/MenuIcons/right-arrow-forward.png -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Browser/NeteaseTVDemo-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #ifndef NeteaseTVDemo_Bridging_Header_h 2 | #define NeteaseTVDemo_Bridging_Header_h 3 | #import "WKBrowserViewController.h" 4 | #endif /* NeteaseTVDemo_Bridging_Header_h */ 5 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Browser/WKBrowserViewController.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | 4 | NS_ASSUME_NONNULL_BEGIN 5 | 6 | @interface WKBrowserViewController : UIViewController 7 | @property (nonatomic, copy) NSString *url; 8 | 9 | @end 10 | 11 | NS_ASSUME_NONNULL_END 12 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Detail/WKDescView.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import SnapKit 4 | class WKDescView: UIControl { 5 | let descLabel = UILabel() 6 | var onPrimaryAction: ((WKDescView) -> Void)? 7 | private let backgroundView = UIView() 8 | init() { 9 | super.init(frame: .zero) 10 | setup() 11 | } 12 | 13 | required init?(coder: NSCoder) { 14 | super.init(coder: coder) 15 | setup() 16 | } 17 | 18 | func setup() { 19 | ({(view: UIView) in 20 | view.backgroundColor = UIColor(white: 0.9, alpha: 0.3) 21 | view.layer.shadowOffset = CGSizeMake(0, 10) 22 | view.layer.shadowOpacity = 0.15 23 | view.layer.shadowRadius = 16.0 24 | view.layer.cornerRadius = 20 25 | view.layer.cornerCurve = .continuous 26 | view.isHidden = !isFocused 27 | addSubview(view) 28 | view.snp.makeConstraints { make in 29 | make.top.bottom.equalToSuperview() 30 | make.left.equalToSuperview().offset(-20) 31 | make.right.equalToSuperview().offset(20) 32 | } 33 | })(backgroundView) 34 | 35 | ({(label: UILabel) in 36 | label.numberOfLines = 0 37 | label.font = UIFont.systemFont(ofSize: 29) 38 | label.textColor = UIColor(named: "titleColor") 39 | label.textAlignment = .center 40 | addSubview(label) 41 | label.snp.makeConstraints { make in 42 | make.leading.trailing.equalToSuperview() 43 | make.centerY.equalToSuperview() 44 | make.bottom.lessThanOrEqualToSuperview().offset(-14) 45 | } 46 | })(descLabel) 47 | } 48 | 49 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 50 | super.didUpdateFocus(in: context, with: coordinator) 51 | backgroundView.isHidden = !isFocused 52 | } 53 | 54 | override func pressesEnded(_ presses: Set, with event: UIPressesEvent?) { 55 | super.pressesEnded(presses, with: event) 56 | if presses.first?.type == .select { 57 | sendActions(for: .primaryActionTriggered) 58 | onPrimaryAction?(self) 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Detail/WKDescViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | class WKDescViewController: UIViewController { 5 | var descStr:String? 6 | @IBOutlet weak var textView: UITextView! 7 | static func creat(desc: String) -> WKDescViewController { 8 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKDescViewController 9 | vc.descStr = desc 10 | return vc 11 | } 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | self.textView.text = self.descStr! 16 | self.textView.panGestureRecognizer.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.indirect.rawValue)] 17 | self.textView.isScrollEnabled = true 18 | self.textView.isUserInteractionEnabled = true 19 | self.textView.isSelectable = true 20 | // Do any additional setup after loading the view. 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Detail/WKMoreView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class WKMoreView: UIControl { 4 | let moreLabel = UILabel() 5 | let moreImageView = UIImageView() 6 | var onPrimaryAction: ((WKMoreView) -> Void)? 7 | private let backgroundView = UIView() 8 | 9 | init() { 10 | super.init(frame: .zero) 11 | setupViews() 12 | } 13 | required init?(coder: NSCoder) { 14 | super.init(coder: coder) 15 | setupViews() 16 | } 17 | 18 | func setupViews() { 19 | ({(view: UIView) in 20 | view.backgroundColor = UIColor(white: 1.0, alpha: 0.9) 21 | view.layer.shadowOffset = CGSizeMake(0, 10) 22 | view.layer.shadowOpacity = 0.15 23 | view.layer.shadowRadius = 16.0 24 | view.layer.cornerRadius = 20 25 | view.layer.cornerCurve = .continuous 26 | view.isHidden = !isFocused 27 | addSubview(view) 28 | view.snp.makeConstraints { make in 29 | make.top.bottom.equalToSuperview() 30 | make.left.equalToSuperview().offset(-20) 31 | make.right.equalToSuperview().offset(20) 32 | } 33 | })(backgroundView) 34 | 35 | ({(label: UILabel) in 36 | label.numberOfLines = 0 37 | // label.text = "更多" 38 | label.font = UIFont.systemFont(ofSize: 60, weight: .medium) 39 | label.textColor = UIColor(named: "titleColor") 40 | label.textAlignment = .center 41 | addSubview(label) 42 | label.snp.makeConstraints { make in 43 | make.leading.equalToSuperview() 44 | make.centerY.equalToSuperview() 45 | } 46 | })(moreLabel) 47 | ({(imageView: UIImageView) in 48 | imageView.image = UIImage(systemName: "chevron.right") 49 | imageView.tintColor = UIColor(named: "titleColor") 50 | addSubview(imageView) 51 | imageView.snp.makeConstraints { make in 52 | // make.right.equalToSuperview().offset(-12) 53 | make.center.equalToSuperview() 54 | make.size.equalTo(CGSize(width: 40, height: 50)) 55 | } 56 | })(moreImageView) 57 | } 58 | 59 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 60 | super.didUpdateFocus(in: context, with: coordinator) 61 | if isFocused { 62 | self.moreLabel.textColor = UIColor.black 63 | self.moreImageView.tintColor = UIColor.black 64 | } else { 65 | self.moreLabel.textColor = UIColor(named: "titleColor") 66 | self.moreImageView.tintColor = UIColor(named: "titleColor") 67 | } 68 | backgroundView.isHidden = !isFocused 69 | } 70 | 71 | override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { 72 | super.pressesBegan(presses, with: event) 73 | for press in presses { 74 | if press.type == .select { 75 | self.transform = CGAffineTransformMakeScale(0.9, 0.9) 76 | } 77 | } 78 | } 79 | 80 | override func pressesEnded(_ presses: Set, with event: UIPressesEvent?) { 81 | super.pressesEnded(presses, with: event) 82 | if presses.first?.type == .select { 83 | sendActions(for: .primaryActionTriggered) 84 | onPrimaryAction?(self) 85 | self.transform = CGAffineTransformMakeScale(1.0, 1.0) 86 | } 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Detail/WKSingerDetailAlbumListVC.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import NeteaseRequest 3 | class WKSingerDetailAlbumListVC: UIViewController { 4 | 5 | @IBOutlet weak var albumCollectionView: UICollectionView! 6 | private var singerId: Int! 7 | private var singerAlbums = [NRAlbumModel]() 8 | static func creat(singerId: Int) -> WKSingerDetailAlbumListVC { 9 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKSingerDetailAlbumListVC 10 | vc.singerId = singerId 11 | return vc 12 | } 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | albumCollectionView.register(WKPlayListCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self)) 16 | albumCollectionView.collectionViewLayout = makeSingerAlbumCollectionViewLayout() 17 | 18 | Task { 19 | await loadData() 20 | albumCollectionView.reloadData() 21 | } 22 | 23 | } 24 | 25 | func loadData() async { 26 | do { 27 | singerAlbums = try await fetchArtistAlbum(cookie: cookie, id: singerId, limit: 100) 28 | } catch { 29 | print(error) 30 | } 31 | } 32 | 33 | func makeSingerAlbumCollectionViewLayout () -> UICollectionViewLayout { 34 | UICollectionViewCompositionalLayout { 35 | [weak self] _, _ in 36 | return self?.makeGridLayoutSection() 37 | } 38 | } 39 | 40 | func makeGridLayoutSection() -> NSCollectionLayoutSection { 41 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 42 | widthDimension: .fractionalWidth(1/2), 43 | heightDimension: .fractionalHeight(1) 44 | )) 45 | let hSpacing: CGFloat = 30 46 | item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: hSpacing, bottom: 0, trailing: hSpacing) 47 | let group = NSCollectionLayoutGroup.horizontalGroup(with: NSCollectionLayoutSize( 48 | widthDimension: .fractionalWidth(1), 49 | heightDimension:.fractionalWidth(0.4) 50 | ), repeatingSubitem: item, count: 2) 51 | let vSpacing: CGFloat = 16 52 | let baseSpacing: CGFloat = 24 53 | group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(baseSpacing), top: .fixed(vSpacing), trailing: .fixed(0), bottom: .fixed(vSpacing)) 54 | let section = NSCollectionLayoutSection(group: group) 55 | if baseSpacing > 0 { 56 | section.contentInsets = NSDirectionalEdgeInsets(top: baseSpacing, leading: 0, bottom: 0, trailing: 0) 57 | } 58 | return section 59 | } 60 | 61 | } 62 | 63 | extension WKSingerDetailAlbumListVC: UICollectionViewDelegate, UICollectionViewDataSource { 64 | 65 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 66 | return singerAlbums.count 67 | } 68 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 69 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self), for: indexPath) as! WKPlayListCollectionViewCell 70 | cell.playListCover.kf.setImage(with: URL(string: singerAlbums[indexPath.row].picUrl ?? "")) 71 | cell.titleLabel.text = singerAlbums[indexPath.row].name ?? "" 72 | return cell 73 | } 74 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 75 | let albumVC = WKAlbumDetailViewController.creat(playListId: (singerAlbums[indexPath.row].id)!) 76 | albumVC.modalPresentationStyle = .blurOverFullScreen 77 | self.present(albumVC, animated: true) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Detail/WKSingerDetailViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import NeteaseRequest 4 | class WKSingerDetailViewController: UIViewController { 5 | 6 | private var singerId: Int! 7 | @IBOutlet weak var singerNameLabel: UILabel! 8 | 9 | @IBOutlet weak var tabBar: UITabBar! 10 | @IBOutlet weak var fansCountLabel: UILabel! 11 | @IBOutlet weak var identifyLabel: UILabel! 12 | @IBOutlet weak var descLabel: UILabel! 13 | @IBOutlet weak var singerImageView: UIImageView! 14 | 15 | @IBOutlet weak var listBgView: UIView! 16 | private var songListVC: WKSongListViewController! 17 | private var albumListVC: WKSingerDetailAlbumListVC! 18 | private var mvListVC: WKSingerMVListVC! 19 | 20 | static func creat(singerId: Int) -> WKSingerDetailViewController { 21 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKSingerDetailViewController 22 | vc.singerId = singerId 23 | vc.songListVC = WKSongListViewController.creat(singerId: singerId) 24 | vc.albumListVC = WKSingerDetailAlbumListVC.creat(singerId: singerId) 25 | vc.mvListVC = WKSingerMVListVC.creat(singerId: singerId) 26 | return vc 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | self.addChild(songListVC) 32 | listBgView.addSubview(songListVC.view) 33 | songListVC.view.snp.makeConstraints { make in 34 | make.edges.equalToSuperview() 35 | } 36 | 37 | self.addChild(albumListVC) 38 | listBgView.addSubview(albumListVC.view) 39 | albumListVC.view.snp.makeConstraints { make in 40 | make.edges.equalToSuperview() 41 | } 42 | albumListVC.view.isHidden = true 43 | 44 | self.addChild(mvListVC) 45 | listBgView.addSubview(mvListVC.view) 46 | mvListVC.view.snp.makeConstraints { make in 47 | make.edges.equalToSuperview() 48 | } 49 | mvListVC.view.isHidden = true 50 | 51 | Task { 52 | await loadSingerDetail() 53 | } 54 | } 55 | 56 | func loadSingerDetail() async { 57 | do { 58 | let singerDetail: NRArtistDetailModel = try await fetchArtistDetail(id: singerId) 59 | self.singerImageView.kf.setImage(with: URL(string: singerDetail.artist?.cover ?? "")) 60 | self.singerNameLabel.text = singerDetail.artist?.name 61 | self.fansCountLabel.text = try await fetchArtistFansCount(id: singerId).fansCnt.toTenThousandString(scale: 2) + "粉丝" 62 | self.identifyLabel.text = singerDetail.identify?.imageDesc 63 | self.descLabel.text = singerDetail.artist?.briefDesc 64 | } catch { 65 | print(error) 66 | } 67 | } 68 | } 69 | 70 | extension WKSingerDetailViewController: UITabBarDelegate { 71 | func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { 72 | if item.title == "单曲" { 73 | songListVC.view.isHidden = false 74 | albumListVC.view.isHidden = true 75 | mvListVC.view.isHidden = true 76 | } else if item.title == "专辑" { 77 | songListVC.view.isHidden = true 78 | albumListVC.view.isHidden = false 79 | mvListVC.view.isHidden = true 80 | } else if item.title == "视频" { 81 | songListVC.view.isHidden = true 82 | albumListVC.view.isHidden = true 83 | mvListVC.view.isHidden = false 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Detail/WKSingerMVListVC.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import NeteaseRequest 4 | class WKSingerMVListVC: UIViewController { 5 | 6 | @IBOutlet weak var collectionView: UICollectionView! 7 | private var singerId: Int! 8 | private var singerMVs = [NRMVListModel]() 9 | static func creat(singerId: Int) -> WKSingerMVListVC { 10 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKSingerMVListVC 11 | vc.singerId = singerId 12 | return vc 13 | } 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | collectionView.register(WKMVCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: WKMVCollectionViewCell.self)) 18 | collectionView.collectionViewLayout = makeSingerMVCollectionViewLayout() 19 | Task { 20 | await loadData() 21 | collectionView.reloadData() 22 | } 23 | } 24 | 25 | func loadData() async { 26 | do { 27 | singerMVs = try await fetchArtistMV(id: singerId) 28 | } catch { 29 | print(error) 30 | } 31 | } 32 | 33 | func makeSingerMVCollectionViewLayout () -> UICollectionViewLayout { 34 | UICollectionViewCompositionalLayout { 35 | [weak self] _, _ in 36 | return self?.makeGridLayoutSection() 37 | } 38 | } 39 | 40 | func makeGridLayoutSection() -> NSCollectionLayoutSection { 41 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 42 | widthDimension: .fractionalWidth(1/2), 43 | heightDimension: .fractionalHeight(1) 44 | )) 45 | let hSpacing: CGFloat = 30 46 | item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: hSpacing, bottom: 0, trailing: hSpacing) 47 | let group = NSCollectionLayoutGroup.horizontalGroup(with: NSCollectionLayoutSize( 48 | widthDimension: .fractionalWidth(1), 49 | heightDimension:.fractionalWidth(0.3) 50 | ), repeatingSubitem: item, count: 2) 51 | let vSpacing: CGFloat = 16 52 | let baseSpacing: CGFloat = 24 53 | group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(baseSpacing), top: .fixed(vSpacing), trailing: .fixed(0), bottom: .fixed(vSpacing)) 54 | let section = NSCollectionLayoutSection(group: group) 55 | if baseSpacing > 0 { 56 | section.contentInsets = NSDirectionalEdgeInsets(top: baseSpacing, leading: 0, bottom: 0, trailing: hSpacing) 57 | } 58 | return section 59 | } 60 | 61 | } 62 | 63 | extension WKSingerMVListVC: UICollectionViewDelegate, UICollectionViewDataSource { 64 | 65 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 66 | return singerMVs.count 67 | } 68 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 69 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: WKMVCollectionViewCell.self), for: indexPath) as! WKMVCollectionViewCell 70 | cell.coverImageView.kf.setImage(with: URL(string: singerMVs[indexPath.row].imgurl16v9 ?? "")) 71 | cell.titleLabel.text = singerMVs[indexPath.row].name 72 | let min = singerMVs[indexPath.row].duration / 1000 / 60 73 | let sec = singerMVs[indexPath.row].duration / 1000 % 60 74 | cell.duartionLabel.text = String(format: "%02d:%02d", min, sec) 75 | return cell 76 | } 77 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 78 | let videoPlayerVC = WKVideoViewController(playInfo: WKPlayInfo(id: singerMVs[indexPath.row].id, r: 1080, isMV: true)) 79 | self.present(videoPlayerVC, animated: true) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Detail/WKSongListViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import NeteaseRequest 4 | class WKSongListViewController: UIViewController { 5 | @IBOutlet weak var tableView: UITableView! 6 | private var singerId: Int! 7 | private var allModels: [CustomAudioModel] = [CustomAudioModel]() 8 | static func creat(singerId: Int) -> WKSongListViewController { 9 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKSongListViewController 10 | vc.singerId = singerId 11 | return vc 12 | } 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | tableView.register(WKSongTableViewCell.self, forCellReuseIdentifier: "WKSongTableViewCell") 17 | Task { 18 | await loadData() 19 | self.tableView.reloadData() 20 | } 21 | } 22 | 23 | func loadData() async { 24 | do { 25 | let songModels: [NRSongModel] = try await fetchArtistSongs(cookie: cookie, id: singerId) 26 | self.allModels.removeAll() 27 | for songModel in songModels { 28 | let model = CustomAudioModel() 29 | model.audioId = songModel.id 30 | model.isFree = 1 31 | model.freeTime = 0 32 | model.audioTitle = songModel.name 33 | model.audioPicUrl = songModel.al?.picUrl 34 | model.transTitle = songModel.tns?.first 35 | model.albumTitle = songModel.al?.name 36 | let min = (songModel.dt ?? 0) / 1000 / 60 37 | let sec = (songModel.dt ?? 0) / 1000 % 60 38 | model.audioTime = String(format: "%d:%02d", min, sec) 39 | if let singerModel = songModel.ar { 40 | model.singer = singerModel.map { $0.name ?? "" }.joined(separator: "/") 41 | } 42 | self.allModels.append(model) 43 | } 44 | } catch { 45 | print(error) 46 | } 47 | } 48 | 49 | } 50 | 51 | 52 | extension WKSongListViewController: UITableViewDelegate, UITableViewDataSource { 53 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 54 | return allModels.count 55 | } 56 | 57 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 58 | let cell = tableView.dequeueReusableCell(withIdentifier: "WKSongTableViewCell", for: indexPath) as! WKSongTableViewCell 59 | cell.indexLabel.text = "\(indexPath.row + 1)" + "." 60 | cell.setModel(allModels[indexPath.row]) 61 | return cell 62 | } 63 | 64 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 65 | if wk_player.isPlaying && wk_player.currentModel?.audioId == self.allModels[indexPath.row].audioId { 66 | let playingVC = WKPlayingViewController.creat() 67 | self.present(playingVC, animated: true) 68 | return 69 | } 70 | // let model: [CustomAudioModel] = [self.allModels[indexPath.row]] 71 | wk_player.allOriginalModels = self.allModels 72 | try? wk_player.play(index: indexPath.row) 73 | let playingVC = WKPlayingViewController.creat() 74 | self.present(playingVC, animated: true) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Detail/WKUserCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | class WKUserCollectionViewCell: UICollectionViewCell { 5 | private let bgView = UIView() 6 | var nameLabel = UILabel() 7 | override init(frame: CGRect) { 8 | super.init(frame: frame) 9 | ({(view: UIView) in 10 | view.backgroundColor = UIColor(white: 0.9, alpha: 0.1) 11 | view.layer.shadowOffset = CGSizeMake(0, 10) 12 | view.layer.shadowOpacity = 0.15 13 | view.layer.shadowRadius = 16.0 14 | view.layer.cornerRadius = 20 15 | view.layer.cornerCurve = .continuous 16 | view.isHidden = !isFocused 17 | addSubview(view) 18 | view.snp.makeConstraints { make in 19 | make.edges.equalToSuperview() 20 | } 21 | })(bgView) 22 | 23 | ({(label: UILabel) in 24 | label.font = .systemFont(ofSize: 30) 25 | label.numberOfLines = 0 26 | label.textAlignment = .center 27 | contentView.addSubview(label) 28 | label.snp.makeConstraints { make in 29 | make.edges.equalToSuperview() 30 | } 31 | 32 | })(nameLabel) 33 | } 34 | 35 | required init?(coder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 40 | super.didUpdateFocus(in: context, with: coordinator) 41 | bgView.isHidden = !isFocused 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Find/WKFindModel.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import NeteaseRequest 4 | struct WKFindModel { 5 | var title: String 6 | var cateInfoModels: [NRCatInfoModel] 7 | init(title: String, cateInfoModels: [NRCatInfoModel]) { 8 | self.title = title 9 | self.cateInfoModels = cateInfoModels 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Login/WKLoginViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import NeteaseRequest 4 | import EFQRCode 5 | class WKLoginViewController: UIViewController { 6 | 7 | @IBOutlet weak var qrCodeImageView: UIImageView! 8 | var timer: Timer? 9 | @IBOutlet weak var tipsLabel: UILabel! 10 | var code: Int? 11 | static func creat() -> WKLoginViewController { 12 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKLoginViewController 13 | vc.modalPresentationStyle = .blurOverFullScreen 14 | return vc 15 | } 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | Task { 20 | await loadQRCode() 21 | } 22 | } 23 | 24 | override func viewWillDisappear(_ animated: Bool) { 25 | super.viewWillDisappear(animated) 26 | stopValidationTimer() 27 | } 28 | 29 | func loadQRCode() async { 30 | guard let key: String = try? await fetchQRKey().unikey else { 31 | showAlert("请完成验证操作(多尝试几次😭)") 32 | return 33 | } 34 | let qrurl: String = try! await fetchQRCode(key: key).qrurl 35 | 36 | if let image = EFQRCode.generate( 37 | for: qrurl, 38 | watermark: UIImage(named: "bge")?.cgImage 39 | ) { 40 | print("Create QRCode image success \(image)") 41 | self.qrCodeImageView.image = UIImage(cgImage: image) 42 | } else { 43 | print("Create QRCode image failed!") 44 | } 45 | self.startValidationTimer(key: key) 46 | 47 | } 48 | 49 | func startValidationTimer(key: String) { 50 | timer = Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { [self] _ in 51 | if code == 803 { 52 | self.stopValidationTimer() 53 | } 54 | Task { 55 | await self.loopValidation(key: key) 56 | } 57 | } 58 | timer?.fire() 59 | print("") 60 | } 61 | 62 | func stopValidationTimer() { 63 | timer?.invalidate() 64 | timer = nil 65 | } 66 | 67 | func loopValidation(key: String) async { 68 | do { 69 | let checkModel: NRQRCodeCheckModel = try await checkQRCode(key: key) 70 | print(checkModel) 71 | 72 | if checkModel.code == 803 { 73 | print("授权成功") 74 | UserDefaults.standard.setValue(checkModel.cookie, forKey: "cookie") 75 | cookie = checkModel.cookie! 76 | NotificationCenter.default.post(name: NSNotification.Name(rawValue: "login"), object: nil, userInfo: nil) 77 | // 78 | do { 79 | let userModel: NRProfileModel = try await fetchAccountInfo(cookie: cookie) 80 | let newAccount = WKUserModel(isSelected: true, user: userModel, cookie: cookie) 81 | var accounts: [WKUserModel] = UserDefaults.standard.codable(forKey: "accounts") ?? [] 82 | accounts.removeAll { $0.user.userId == userModel.userId } 83 | accounts.append(newAccount) 84 | UserDefaults.standard.set(codable: accounts, forKey: "accounts") 85 | UserDefaults.standard.set(codable: accounts.last?.user, forKey: "userModel") 86 | } catch { 87 | print(error) 88 | } 89 | 90 | self.dismiss(animated: true) 91 | AppDelegate.shared.showTabBar() 92 | } else if checkModel.code == 800 { 93 | print("二维码过期") 94 | tipsLabel.text = "二维码过期,请重新扫码" 95 | self.showAlert("二维码过期,请重新扫码") 96 | timer?.invalidate() 97 | await loadQRCode() 98 | } else if checkModel.code == 801 { 99 | print("等待扫码") 100 | tipsLabel.text = "等待扫码" 101 | } else if checkModel.code == 802 { 102 | print("等待确认") 103 | tipsLabel.text = "等待确认" 104 | } 105 | } catch { 106 | print(error) 107 | showAlert(error.localizedDescription) 108 | await loopValidation(key: key) 109 | } 110 | 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Login/WKUserModel.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import NeteaseRequest 4 | 5 | struct WKUserModel: Codable { 6 | var isSelected: Bool 7 | var user: NRProfileModel 8 | var cookie: String 9 | 10 | init(isSelected: Bool, user: NRProfileModel, cookie: String) { 11 | self.isSelected = isSelected 12 | self.user = user 13 | self.cookie = cookie 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Player/CustomAudioModel.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import NeteaseRequest 4 | public class CustomAudioModel: WKPlayerDataSource,Codable { 5 | 6 | /** 需要具体描述音频数据源的播放类型,共有3种类型, 7 | /** 没有权限*/ 8 | case noPermission 9 | /** 完整播放*/ 10 | case full 11 | /** 部分播放,参数为允许播放时长*/ 12 | case partly(length: UInt) 13 | */ 14 | public var wk_sourceType: WKPlayerSourceType { 15 | get { 16 | if isFree == 1 { 17 | return .full 18 | } else if let length = freeTime { 19 | return .partly(length: length) 20 | } else { 21 | return .noPermission 22 | } 23 | } 24 | } 25 | 26 | public var wk_playURL: String? { 27 | get { 28 | return audioUrl 29 | } 30 | } 31 | 32 | public var wk_audioId: Int? { 33 | get { 34 | return audioId 35 | } 36 | } 37 | 38 | public var wk_audioPic: String? { 39 | get { 40 | return audioPicUrl 41 | } 42 | } 43 | 44 | 45 | public var wk_sourceName: String? { 46 | get { 47 | return audioTitle 48 | } 49 | } 50 | 51 | public var wk_singerName: String? { 52 | get { 53 | return singer 54 | } 55 | } 56 | /** 歌手*/ 57 | var singer: String? 58 | /** 音频id*/ 59 | var audioId: Int? 60 | /** 音频地址*/ 61 | var audioUrl: String? 62 | /** 音频标题*/ 63 | var audioTitle: String? 64 | var albumTitle: String? 65 | var transTitle: String? 66 | var fee: NRFee? 67 | var like: Bool? 68 | 69 | /** 音频质量*/ 70 | var audioQuality: String? 71 | 72 | /** 音频时长*/ 73 | var audioTime: String? 74 | 75 | /** 音频封面*/ 76 | var audioPicUrl: String? 77 | 78 | /** 是否可以完整播放*/ 79 | var isFree: Int? 80 | /** 不可以完整播放时能播放的秒数*/ 81 | var freeTime: UInt? 82 | } 83 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Player/WKPlayerError.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | public enum WKPlayerError: Error { 5 | case dataSourceError(reason: DataSourceError) 6 | case networkError(reason: NetworkError) 7 | // case functionError 8 | case playerStatusError(reason: PlayerStatusError) 9 | 10 | ///数据源错误 11 | public enum DataSourceError { 12 | case lackOfDataSource 13 | case noPermission 14 | case invalidDataSource 15 | case noLastDataSource 16 | case noNextDataSource 17 | case needBuyAlbum 18 | case needVip 19 | } 20 | 21 | ///网络错误 22 | public enum NetworkError { 23 | ///网络不可用 24 | case notReachable 25 | ///网络超时 26 | case timeout 27 | } 28 | 29 | 30 | ///播放器状态异常 31 | public enum PlayerStatusError { 32 | ///未知异常 33 | case unknown 34 | ///播放器状态异常,无法播放 35 | case failed 36 | } 37 | 38 | } 39 | 40 | extension WKPlayerError: LocalizedError { 41 | public var errorDescription: String? { 42 | switch self { 43 | case .dataSourceError(let reason): 44 | return reason.localizedDescription 45 | case .networkError(let reason): 46 | return reason.localizedDescription 47 | case .playerStatusError(let reason): 48 | return reason.localizedDescription 49 | } 50 | } 51 | } 52 | 53 | extension WKPlayerError.DataSourceError { 54 | public var localizedDescription: String { 55 | switch self { 56 | case .lackOfDataSource: 57 | return "缺少数据源。" 58 | case .noPermission: 59 | return "暂无版权。" 60 | case .invalidDataSource: 61 | return "无效数据源,没有播放地址的数据源。" 62 | case .noLastDataSource: 63 | return "没有上一条数据。" 64 | case .noNextDataSource: 65 | return "没有下一条数据。" 66 | case .needBuyAlbum: 67 | return "需要到网易云音乐购买专辑。" 68 | case .needVip: 69 | return "需要网易云音乐 VIP 权限。" 70 | } 71 | } 72 | } 73 | 74 | extension WKPlayerError.NetworkError { 75 | public var localizedDescription: String { 76 | switch self { 77 | case .notReachable: 78 | return "网络无法访问。" 79 | case .timeout: 80 | return "网络超时。" 81 | } 82 | } 83 | } 84 | 85 | extension WKPlayerError.PlayerStatusError { 86 | public var localizedDescription: String { 87 | switch self { 88 | case .unknown: 89 | return "播放时出现未知错误。" 90 | case .failed: 91 | return "播放器播放失败。" 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Player/WKPlayerSettings.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | 5 | public class WKPlayerSettings { 6 | public var `default`: [String: Any] { 7 | set { 8 | _default = newValue 9 | } 10 | get { 11 | if let temp = _default { 12 | return temp 13 | } else { 14 | if let dict = UserDefaults.standard.value(forKey: kSettings) as? [String: Any] { 15 | _default = dict 16 | return dict 17 | } else { 18 | let dict = [kRate: Float(1)] 19 | UserDefaults.standard.setValue(dict, forKey: kSettings) 20 | UserDefaults.standard.synchronize() 21 | _default = dict 22 | return dict 23 | } 24 | } 25 | 26 | } 27 | } 28 | 29 | public var rate: Float { 30 | get { 31 | if let temp = _rate { 32 | return temp 33 | } else { 34 | _rate = self.default[kRate] as? Float 35 | } 36 | return _rate ?? 1 37 | } 38 | 39 | set { 40 | _rate = newValue 41 | wk_player.player?.rate = newValue 42 | self.default[kRate] = newValue 43 | update() 44 | } 45 | } 46 | 47 | private var _default: [String: Any]? 48 | private var _rate: Float? 49 | private let kRate = "WKPlayerRate" 50 | private let kSettings = "WKPlayerSettings" 51 | 52 | private func update() { 53 | UserDefaults.standard.setValue(self.default, forKey: kSettings) 54 | UserDefaults.standard.synchronize() 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Player/WKPlayerStateModel.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | 5 | public class WKPlayerStateModel { 6 | /** 播放状态*/ 7 | public var state: WKPlayerState = .idle 8 | /** 播放进度*/ 9 | public var progress: Float = 0 10 | /** 缓冲进度*/ 11 | public var buffer: Float = 0 12 | /** 当前播放秒数*/ 13 | public var current: UInt = 0 14 | /** 总时长*/ 15 | public var duration: UInt = 0 16 | // /** 最大历史进度*/ 17 | // var maxHistoricalProgress: Float = 0 18 | } 19 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Playing/WKCommentTableViewCell.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | class WKCommentTableViewCell: UITableViewCell { 5 | 6 | var avatarImageView = UIImageView() 7 | var nameLabel = UILabel() 8 | var commentLabel = UILabel() 9 | 10 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 11 | super.init(style: style, reuseIdentifier: reuseIdentifier) 12 | 13 | ({(view: UIImageView) in 14 | view.layer.cornerRadius = 15 15 | view.clipsToBounds = true 16 | view.contentMode = .scaleAspectFill 17 | contentView.addSubview(view) 18 | view.snp.makeConstraints { make in 19 | make.left.equalToSuperview().offset(30) 20 | make.top.equalToSuperview().offset(15) 21 | make.size.equalTo(80) 22 | } 23 | })(avatarImageView) 24 | 25 | ({(label: UILabel)in 26 | label.font = .systemFont(ofSize: 30) 27 | label.numberOfLines = 0 28 | contentView.addSubview(label) 29 | label.snp.makeConstraints { make in 30 | make.left.equalTo(avatarImageView.snp.right).offset(30) 31 | make.top.equalTo(avatarImageView) 32 | make.right.equalToSuperview().offset(-30) 33 | } 34 | })(nameLabel) 35 | 36 | 37 | ({(label: UILabel)in 38 | label.numberOfLines = 0 39 | contentView.addSubview(label) 40 | label.snp.makeConstraints { make in 41 | make.left.equalTo(avatarImageView.snp.right).offset(30) 42 | make.top.equalTo(nameLabel.snp.bottom).offset(15) 43 | make.right.bottom.equalToSuperview().offset(-30) 44 | } 45 | })(commentLabel) 46 | } 47 | 48 | required init?(coder: NSCoder) { 49 | fatalError("init(coder:) has not been implemented") 50 | } 51 | 52 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 53 | super.didUpdateFocus(in: context, with: coordinator) 54 | if isFocused { 55 | nameLabel.textColor = .black 56 | commentLabel.textColor = .black 57 | } else { 58 | nameLabel.textColor = UIColor(dynamicProvider: { (traitCollection: UITraitCollection) -> UIColor in 59 | return traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark ? 60 | .white : .black 61 | }) 62 | commentLabel.textColor = UIColor(dynamicProvider: { (traitCollection: UITraitCollection) -> UIColor in 63 | return traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark ? 64 | .white : .black 65 | }) 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Playing/WKCommentViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import NeteaseRequest 3 | import Kingfisher 4 | class WKCommentViewController: UIViewController { 5 | 6 | @IBOutlet weak var tableView: UITableView! 7 | var songId: Int? 8 | var commentModels = [NRCommentModel]() 9 | static func creat(songId: Int) -> WKCommentViewController { 10 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKCommentViewController 11 | vc.songId = songId 12 | return vc 13 | } 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | tableView.register(WKCommentTableViewCell.self, forCellReuseIdentifier: "WKCommentTableViewCell") 18 | Task { 19 | await loadComment() 20 | } 21 | } 22 | 23 | func loadComment() async { 24 | do { 25 | commentModels = try await fetchMusicHotComment(id: self.songId!, cookie: cookie, limit: 1000) 26 | print(commentModels) 27 | tableView.reloadData() 28 | } catch { 29 | print(error) 30 | } 31 | } 32 | 33 | } 34 | 35 | extension WKCommentViewController: UITableViewDelegate, UITableViewDataSource { 36 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 37 | return commentModels.count 38 | } 39 | 40 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 41 | let cell = tableView.dequeueReusableCell(withIdentifier: "WKCommentTableViewCell", for: indexPath) as! WKCommentTableViewCell 42 | cell.nameLabel.text = commentModels[indexPath.row].user.nickname 43 | cell.commentLabel.text = commentModels[indexPath.row].content 44 | cell.avatarImageView.kf.setImage(with: URL(string: commentModels[indexPath.row].user.avatarUrl)) 45 | return cell 46 | } 47 | 48 | 49 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 50 | let vc = WKDescViewController.creat(desc: commentModels[indexPath.row].content) 51 | self.present(vc, animated: true) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Playing/WKLyricProgressLabel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class WKLyricProgressLabel: UILabel { 4 | var progress: CGFloat = 0.0 { 5 | didSet { 6 | if progress < 0 { 7 | progress = 0 8 | } else if progress > 1 { 9 | progress = 1 10 | } 11 | setNeedsDisplay() 12 | } 13 | } 14 | 15 | override func draw(_ rect: CGRect) { 16 | super.draw(rect) 17 | 18 | guard let context = UIGraphicsGetCurrentContext() else { return } 19 | 20 | let fillRect = CGRect(x: 0, y: 0, width: self.bounds.size.width * self.progress, height: self.bounds.size.height) 21 | 22 | UIColor(red: 45/255, green: 185/255, blue: 105/255, alpha: 1.0).set() 23 | context.setBlendMode(.sourceIn) 24 | context.fill(fillRect) 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Playing/WKLyricTableViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SnapKit 3 | class WKLyricTableViewCell: UITableViewCell { 4 | var contentLabel: UILabel? 5 | 6 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 7 | super.init(style: style, reuseIdentifier: reuseIdentifier) 8 | self.contentLabel = UILabel() 9 | self.contentLabel?.numberOfLines = 0 10 | self.contentLabel?.textAlignment = .left 11 | self.contentView.addSubview(self.contentLabel!) 12 | self.contentLabel?.snp.makeConstraints({ make in 13 | make.edges.equalToSuperview() 14 | }) 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | 22 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 23 | super.didUpdateFocus(in: context, with: coordinator) 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Playing/WKSlider.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public protocol WKSliderDelegate: AnyObject { 4 | func forward() 5 | func backward() 6 | func playOrPause() 7 | } 8 | 9 | class WKSlider: UIProgressView { 10 | 11 | public weak var delegate: WKSliderDelegate? 12 | 13 | override func becomeFirstResponder() -> Bool { 14 | return true 15 | } 16 | 17 | override var canBecomeFocused: Bool { 18 | return true 19 | } 20 | 21 | override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { 22 | super.pressesBegan(presses, with: event) 23 | for press in presses { 24 | if press.type == .rightArrow { 25 | delegate?.forward() 26 | } else if press.type == .leftArrow { 27 | delegate?.backward() 28 | } else if press.type == .select { 29 | delegate?.playOrPause() 30 | self.transform = CGAffineTransformMakeScale(1.0, 1.0) 31 | } 32 | } 33 | } 34 | 35 | override func pressesEnded(_ presses: Set, with event: UIPressesEvent?) { 36 | for press in presses { 37 | if press.type == .select { 38 | self.transform = CGAffineTransformMakeScale(1.05, 1.3) 39 | } 40 | } 41 | } 42 | 43 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 44 | if isFocused { 45 | coordinator.addCoordinatedAnimations { 46 | self.transform = CGAffineTransformMakeScale(1.05, 1.3) 47 | } 48 | } else { 49 | coordinator.addCoordinatedAnimations { 50 | self.transform = CGAffineTransformIdentity 51 | } 52 | } 53 | } 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKAboutViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | class WKAboutViewController: UIViewController { 5 | @IBOutlet weak var tableView: UITableView! 6 | let cells = [["title":"GitHub","subtitle":"https://github.com/ZhangDo/NeteaseTVDemo"]] 7 | static func creat() -> WKAboutViewController { 8 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKAboutViewController 9 | return vc 10 | } 11 | override func viewDidLoad() { 12 | super.viewDidLoad() 13 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 14 | } 15 | 16 | } 17 | 18 | extension WKAboutViewController: UITableViewDelegate, UITableViewDataSource { 19 | func numberOfSections(in tableView: UITableView) -> Int { 20 | return 1 21 | } 22 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 23 | return cells.count 24 | } 25 | 26 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 27 | let cell = UITableViewCell(style: .value1, reuseIdentifier: "cell") 28 | // cell.accessoryType = .disclosureIndicator 29 | var content = cell.defaultContentConfiguration() 30 | content.text = cells[indexPath.row]["title"] 31 | content.secondaryText = cells[indexPath.row]["subtitle"] 32 | cell.contentConfiguration = content 33 | return cell 34 | } 35 | 36 | 37 | 38 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 39 | let vc = WKBrowserViewController() 40 | vc.modalPresentationStyle = .blurOverFullScreen 41 | vc.url = cells[indexPath.row]["subtitle"]! 42 | self.present(vc, animated: true) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKAccountCell.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import TVUIKit 4 | import NeteaseRequest 5 | 6 | class WKAccountCell: UICollectionViewCell { 7 | 8 | let userView = TVMonogramView() 9 | 10 | override init(frame: CGRect) { 11 | super.init(frame: frame) 12 | ({(view: TVMonogramView) in 13 | view.image = UIImage(named: "bgImage") 14 | view.title = "名字" 15 | self.contentView.addSubview(view) 16 | view.snp.makeConstraints { make in 17 | make.edges.equalTo(self.contentView) 18 | } 19 | })(userView) 20 | 21 | } 22 | 23 | 24 | required init?(coder: NSCoder) { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | 28 | // func loadData(with model: NRProfileModel, isAdd: Bool) { 29 | // if isAdd { 30 | // self.userView.image = UIImage(systemName: "person.crop.circle.badge.plus") 31 | // } else { 32 | // self.userView.image = UIImage(named: "bgImage") 33 | // } 34 | // } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKAvatarView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class WKAvatarView: UIControl { 4 | var onPrimaryAction: ((WKAvatarView) -> Void)? 5 | var imageView = UIImageView() 6 | init() { 7 | super.init(frame: .zero) 8 | setup() 9 | } 10 | 11 | required init?(coder: NSCoder) { 12 | super.init(coder: coder) 13 | setup() 14 | } 15 | 16 | func setup() { 17 | ({(view: UIImageView) in 18 | view.image = UIImage(named: "bgImage") 19 | view.layer.cornerRadius = 100 20 | view.layer.masksToBounds = true 21 | addSubview(view) 22 | view.snp.makeConstraints { make in 23 | make.edges.equalToSuperview() 24 | } 25 | })(imageView) 26 | } 27 | 28 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 29 | super.didUpdateFocus(in: context, with: coordinator) 30 | if isFocused { 31 | coordinator.addCoordinatedAnimations { 32 | self.transform = CGAffineTransformMakeScale(1.1, 1.1) 33 | } 34 | } else { 35 | coordinator.addCoordinatedAnimations { 36 | self.transform = CGAffineTransformMakeScale(1.0, 1.0) 37 | } 38 | } 39 | } 40 | override func pressesEnded(_ presses: Set, with event: UIPressesEvent?) { 41 | super.pressesEnded(presses, with: event) 42 | if presses.first?.type == .select { 43 | sendActions(for: .primaryActionTriggered) 44 | onPrimaryAction?(self) 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKMyCloudSongVC.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import NeteaseRequest 4 | 5 | class WKMyCloudSongVC: UIViewController { 6 | @IBOutlet weak var tableView: UITableView! 7 | private var allModels: [CustomAudioModel] = [CustomAudioModel]() 8 | 9 | static func creat() -> WKMyCloudSongVC { 10 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKMyCloudSongVC 11 | return vc 12 | } 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | tableView.register(WKSongTableViewCell.self, forCellReuseIdentifier: "WKSongTableViewCell") 16 | Task { 17 | await loadData() 18 | self.tableView.reloadData() 19 | } 20 | // Do any additional setup after loading the view. 21 | } 22 | 23 | func loadData() async { 24 | do { 25 | let cloudDataModels: [NRUserCloudDataModel] = try await fetchUserCloudData(cookie: cookie, limit: 1000) 26 | self.allModels.removeAll() 27 | for cloudDataModel in cloudDataModels { 28 | let model = CustomAudioModel() 29 | model.audioId = cloudDataModel.songId 30 | model.like = likeIds.contains(cloudDataModel.songId) 31 | model.isFree = 1 32 | // model.fee = cloudDataModel.fee 33 | model.freeTime = 0 34 | model.audioTitle = cloudDataModel.songName 35 | model.audioPicUrl = cloudDataModel.simpleSong.al?.picUrl 36 | model.transTitle = cloudDataModel.simpleSong.tns?.first 37 | model.albumTitle = cloudDataModel.album ?? "" 38 | let min = (cloudDataModel.simpleSong.dt ?? 0) / 1000 / 60 39 | let sec = (cloudDataModel.simpleSong.dt ?? 0) / 1000 % 60 40 | model.audioTime = String(format: "%d:%02d", min, sec) 41 | if let singerModel = cloudDataModel.simpleSong.ar { 42 | model.singer = singerModel.map { $0.name ?? "" }.joined(separator: "/") 43 | } 44 | self.allModels.append(model) 45 | } 46 | } catch { 47 | print(error.localizedDescription) 48 | } 49 | } 50 | 51 | 52 | } 53 | 54 | extension WKMyCloudSongVC: UITableViewDelegate, UITableViewDataSource { 55 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 56 | return allModels.count 57 | } 58 | 59 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 60 | let cell = tableView.dequeueReusableCell(withIdentifier: "WKSongTableViewCell", for: indexPath) as! WKSongTableViewCell 61 | cell.indexLabel.text = "\(indexPath.row + 1)" + "." 62 | cell.setModel(allModels[indexPath.row]) 63 | cell.albumLabel.text = (allModels[indexPath.row].singer ?? "") + " - " + (allModels[indexPath.row].albumTitle ?? "") 64 | return cell 65 | } 66 | 67 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 68 | if wk_player.isPlaying && wk_player.currentModel?.audioId == self.allModels[indexPath.row].audioId { 69 | let playingVC = WKPlayingViewController.creat() 70 | self.present(playingVC, animated: true) 71 | return 72 | } 73 | wk_player.allOriginalModels = self.allModels 74 | try? wk_player.play(index: indexPath.row) 75 | let playingVC = WKPlayingViewController.creat() 76 | self.present(playingVC, animated: true) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKMyCollectionAlbumVC.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import NeteaseRequest 3 | class WKMyCollectionAlbumVC: UIViewController { 4 | 5 | @IBOutlet weak var collectionView: UICollectionView! 6 | fileprivate var albumModels = [NRAlbumModel]() 7 | 8 | static func creat() -> WKMyCollectionAlbumVC { 9 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKMyCollectionAlbumVC 10 | return vc 11 | } 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | collectionView.register(WKPlayListCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self)) 17 | collectionView.collectionViewLayout = makeRencentPlaylistCollectionViewLayout() 18 | 19 | Task { 20 | await loadData() 21 | } 22 | 23 | } 24 | 25 | func loadData() async { 26 | do { 27 | albumModels = try await fetchAlbumSublist(cookie: cookie, limit: 100, offset: 0) 28 | self.collectionView.reloadData() 29 | } catch { 30 | print(error) 31 | } 32 | } 33 | } 34 | 35 | extension WKMyCollectionAlbumVC: UICollectionViewDelegate, UICollectionViewDataSource { 36 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 37 | return albumModels.count 38 | } 39 | 40 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 41 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self), for: indexPath) as! WKPlayListCollectionViewCell 42 | cell.playListCover.kf.setImage(with: URL(string: albumModels[indexPath.row].picUrl!)) 43 | cell.titleLabel.text = albumModels[indexPath.row].name 44 | return cell 45 | } 46 | 47 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 48 | let albumVC = WKAlbumDetailViewController.creat(playListId: (albumModels[indexPath.row].id)!) 49 | albumVC.modalPresentationStyle = .blurOverFullScreen 50 | self.present(albumVC, animated: true) 51 | } 52 | } 53 | 54 | 55 | extension WKMyCollectionAlbumVC { 56 | func makeRencentPlaylistCollectionViewLayout () -> UICollectionViewLayout { 57 | UICollectionViewCompositionalLayout { 58 | [weak self] _, _ in 59 | return self?.makeGridLayoutSection() 60 | } 61 | } 62 | 63 | 64 | func makeGridLayoutSection() -> NSCollectionLayoutSection { 65 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 66 | widthDimension: .fractionalWidth(1/3), 67 | heightDimension: .fractionalHeight(1) 68 | )) 69 | let hSpacing: CGFloat = 30 70 | item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: hSpacing, bottom: 0, trailing: hSpacing) 71 | let group = NSCollectionLayoutGroup.horizontalGroup(with: NSCollectionLayoutSize( 72 | widthDimension: .fractionalWidth(1), 73 | heightDimension:.fractionalWidth(1/3) 74 | ), repeatingSubitem: item, count: 3) 75 | let vSpacing: CGFloat = 16 76 | let baseSpacing: CGFloat = 24 77 | group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(baseSpacing), top: .fixed(vSpacing), trailing: .fixed(0), bottom: .fixed(vSpacing)) 78 | let section = NSCollectionLayoutSection(group: group) 79 | if baseSpacing > 0 { 80 | section.contentInsets = NSDirectionalEdgeInsets(top: baseSpacing, leading: 0, bottom: 0, trailing: 0) 81 | } 82 | return section 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKMyCollectionMVVC.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import NeteaseRequest 4 | 5 | class WKMyCollectionMVVC: UIViewController { 6 | @IBOutlet weak var collectionView: UICollectionView! 7 | fileprivate var mvSublistModels = [NRMVSublistModel]() 8 | 9 | static func creat() -> WKMyCollectionMVVC { 10 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKMyCollectionMVVC 11 | return vc 12 | } 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | collectionView.register(WKMVCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: WKMVCollectionViewCell.self)) 17 | collectionView.collectionViewLayout = makeVideoCollectionViewLayout() 18 | Task { 19 | await loadCollectionMVData() 20 | } 21 | } 22 | 23 | func loadCollectionMVData() async { 24 | do { 25 | mvSublistModels = try await fetchMVSublist(cookie: cookie).filter({ model in 26 | return model.type == 0 27 | }) 28 | collectionView.reloadData() 29 | } catch { 30 | print(error) 31 | } 32 | } 33 | 34 | func makeVideoCollectionViewLayout () -> UICollectionViewLayout { 35 | UICollectionViewCompositionalLayout { 36 | [weak self] _, _ in 37 | return self?.makeGridLayoutSection() 38 | } 39 | } 40 | 41 | func makeGridLayoutSection() -> NSCollectionLayoutSection { 42 | 43 | // let style = styleOverride ?? Settings.displayStyle 44 | // let heightDimension = NSCollectionLayoutDimension.estimated(380) 45 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 46 | widthDimension: .fractionalWidth(1/3), 47 | heightDimension: .fractionalHeight(1) 48 | )) 49 | let hSpacing: CGFloat = 30 50 | item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: hSpacing, bottom: 0, trailing: hSpacing) 51 | let group = NSCollectionLayoutGroup.horizontalGroup(with: NSCollectionLayoutSize( 52 | widthDimension: .fractionalWidth(1), 53 | heightDimension:.fractionalWidth(1/5) 54 | ), repeatingSubitem: item, count: 3) 55 | let vSpacing: CGFloat = 16 56 | let baseSpacing: CGFloat = 24 57 | group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(baseSpacing), top: .fixed(vSpacing), trailing: .fixed(0), bottom: .fixed(vSpacing)) 58 | let section = NSCollectionLayoutSection(group: group) 59 | if baseSpacing > 0 { 60 | section.contentInsets = NSDirectionalEdgeInsets(top: baseSpacing, leading: 0, bottom: 0, trailing: hSpacing) 61 | } 62 | return section 63 | } 64 | } 65 | extension WKMyCollectionMVVC: UICollectionViewDelegate, UICollectionViewDataSource { 66 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 67 | return mvSublistModels.count 68 | } 69 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 70 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: WKMVCollectionViewCell.self), for: indexPath) as! WKMVCollectionViewCell 71 | cell.coverImageView.kf.setImage(with: URL(string: mvSublistModels[indexPath.row].coverUrl)) 72 | cell.titleLabel.text = mvSublistModels[indexPath.row].title 73 | let min = mvSublistModels[indexPath.row].durationms / 1000 / 60 74 | let sec = mvSublistModels[indexPath.row].durationms / 1000 % 60 75 | cell.duartionLabel.text = String(format: "%02d:%02d", min, sec) 76 | return cell 77 | } 78 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 79 | let videoPlayerVC = WKVideoViewController(playInfo: WKPlayInfo(id: Int(mvSublistModels[indexPath.row].vid)!, r: 1080, isMV: true)) 80 | self.present(videoPlayerVC, animated: true) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKMyCollectionPlaylistVC.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import NeteaseRequest 3 | class WKMyCollectionPlaylistVC: UIViewController { 4 | 5 | @IBOutlet weak var collectionView: UICollectionView! 6 | var playList = [NRPlayListModel]() 7 | static func creat() -> WKMyCollectionPlaylistVC { 8 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKMyCollectionPlaylistVC 9 | return vc 10 | } 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | collectionView.register(WKPlayListCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self)) 16 | collectionView.collectionViewLayout = makeRencentPlaylistCollectionViewLayout() 17 | Task { 18 | await loadData() 19 | } 20 | } 21 | 22 | func loadData() async { 23 | do { 24 | if let userModel: NRProfileModel = UserDefaults.standard.codable(forKey: "userModel") { 25 | playList = try await fetchUserPlaylist(cookie: cookie, uid: userModel.userId, limit: 1000).filter { model in 26 | model.userId != userModel.userId 27 | } 28 | collectionView.reloadData() 29 | } 30 | } catch { 31 | print(error) 32 | } 33 | } 34 | 35 | } 36 | 37 | 38 | extension WKMyCollectionPlaylistVC { 39 | 40 | func makeRencentPlaylistCollectionViewLayout () -> UICollectionViewLayout { 41 | UICollectionViewCompositionalLayout { 42 | [weak self] _, _ in 43 | return self?.makeGridLayoutSection() 44 | } 45 | } 46 | 47 | 48 | func makeGridLayoutSection() -> NSCollectionLayoutSection { 49 | 50 | // let style = styleOverride ?? Settings.displayStyle 51 | // let heightDimension = NSCollectionLayoutDimension.estimated(380) 52 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 53 | widthDimension: .fractionalWidth(1/3), 54 | heightDimension: .fractionalHeight(1) 55 | )) 56 | let hSpacing: CGFloat = 30 57 | item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: hSpacing, bottom: 0, trailing: hSpacing) 58 | let group = NSCollectionLayoutGroup.horizontalGroup(with: NSCollectionLayoutSize( 59 | widthDimension: .fractionalWidth(1), 60 | heightDimension:.fractionalWidth(1/3) 61 | ), repeatingSubitem: item, count: 3) 62 | let vSpacing: CGFloat = 16 63 | let baseSpacing: CGFloat = 24 64 | group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(baseSpacing), top: .fixed(vSpacing), trailing: .fixed(0), bottom: .fixed(vSpacing)) 65 | let section = NSCollectionLayoutSection(group: group) 66 | if baseSpacing > 0 { 67 | section.contentInsets = NSDirectionalEdgeInsets(top: baseSpacing, leading: 0, bottom: 0, trailing: 0) 68 | } 69 | return section 70 | } 71 | } 72 | 73 | extension WKMyCollectionPlaylistVC: UICollectionViewDelegate, UICollectionViewDataSource { 74 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 75 | return playList.count 76 | } 77 | 78 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 79 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self), for: indexPath) as! WKPlayListCollectionViewCell 80 | cell.playListCover.kf.setImage(with: URL(string: playList[indexPath.row].coverImgUrl!)) 81 | cell.titleLabel.text = playList[indexPath.row].name 82 | return cell 83 | } 84 | 85 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 86 | let playListDetaiVC = WKPlayListDetailViewController.creat(playListId: playList[indexPath.row].id) 87 | playListDetaiVC.modalPresentationStyle = .blurOverFullScreen 88 | self.present(playListDetaiVC, animated: true) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKMyCollectionPodcastVC.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import NeteaseRequest 4 | class WKMyCollectionPodcastVC: UIViewController { 5 | 6 | @IBOutlet weak var collectionView: UICollectionView! 7 | fileprivate var podcastModels = [NRDJRadioModel]() 8 | static func creat() -> WKMyCollectionPodcastVC { 9 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKMyCollectionPodcastVC 10 | return vc 11 | } 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | collectionView.register(WKPlayListCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self)) 16 | collectionView.collectionViewLayout = makeRecentPodcastCollectionViewLayout() 17 | Task { 18 | await loadData() 19 | } 20 | } 21 | 22 | func loadData() async { 23 | do { 24 | podcastModels = try await fetchDjSublist(cookie: cookie, limit: 1000) 25 | collectionView.reloadData() 26 | } catch { 27 | 28 | } 29 | } 30 | 31 | } 32 | 33 | extension WKMyCollectionPodcastVC: UICollectionViewDelegate, UICollectionViewDataSource { 34 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 35 | return podcastModels.count 36 | } 37 | 38 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 39 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self), for: indexPath) as! WKPlayListCollectionViewCell 40 | cell.playListCover.kf.setImage(with: URL(string: podcastModels[indexPath.row].picUrl)) 41 | cell.titleLabel.text = podcastModels[indexPath.row].name 42 | return cell 43 | } 44 | 45 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 46 | let playListDetaiVC = WKPodcastDetailViewController.creat(djRadioModel: podcastModels[indexPath.row]) 47 | playListDetaiVC.modalPresentationStyle = .blurOverFullScreen 48 | self.present(playListDetaiVC, animated: true) 49 | } 50 | } 51 | 52 | extension WKMyCollectionPodcastVC { 53 | 54 | func makeRecentPodcastCollectionViewLayout () -> UICollectionViewLayout { 55 | UICollectionViewCompositionalLayout { 56 | [weak self] _, _ in 57 | return self?.makeGridLayoutSection() 58 | } 59 | } 60 | 61 | 62 | func makeGridLayoutSection() -> NSCollectionLayoutSection { 63 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 64 | widthDimension: .fractionalWidth(1/3), 65 | heightDimension: .fractionalHeight(1) 66 | )) 67 | let hSpacing: CGFloat = 30 68 | item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: hSpacing, bottom: 0, trailing: hSpacing) 69 | let group = NSCollectionLayoutGroup.horizontalGroup(with: NSCollectionLayoutSize( 70 | widthDimension: .fractionalWidth(1), 71 | heightDimension:.fractionalWidth(1/3) 72 | ), repeatingSubitem: item, count: 3) 73 | let vSpacing: CGFloat = 16 74 | let baseSpacing: CGFloat = 24 75 | group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(baseSpacing), top: .fixed(vSpacing), trailing: .fixed(0), bottom: .fixed(vSpacing)) 76 | let section = NSCollectionLayoutSection(group: group) 77 | if baseSpacing > 0 { 78 | section.contentInsets = NSDirectionalEdgeInsets(top: baseSpacing, leading: 0, bottom: 0, trailing: 0) 79 | } 80 | return section 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKMyCollectionVC.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class WKMyCollectionVC: UIViewController { 4 | @IBOutlet weak var contentView: UIView! 5 | fileprivate var myCollectionPlaylistVC = WKMyCollectionPlaylistVC.creat() 6 | fileprivate var myCollectionAlbumVC = WKMyCollectionAlbumVC.creat() 7 | fileprivate var myCollectionPodcastVC = WKMyCollectionPodcastVC.creat() 8 | fileprivate var myCollectionMVVC = WKMyCollectionMVVC.creat() 9 | 10 | static func creat() -> WKMyCollectionVC { 11 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKMyCollectionVC 12 | return vc 13 | } 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | addChild(myCollectionPlaylistVC) 18 | contentView.addSubview(myCollectionPlaylistVC.view) 19 | myCollectionPlaylistVC.view.snp.makeConstraints { make in 20 | make.edges.equalToSuperview() 21 | } 22 | 23 | addChild(myCollectionAlbumVC) 24 | contentView.addSubview(myCollectionAlbumVC.view) 25 | myCollectionAlbumVC.view.snp.makeConstraints { make in 26 | make.edges.equalToSuperview() 27 | } 28 | 29 | addChild(myCollectionPodcastVC) 30 | contentView.addSubview(myCollectionPodcastVC.view) 31 | myCollectionPodcastVC.view.snp.makeConstraints { make in 32 | make.edges.equalToSuperview() 33 | } 34 | addChild(myCollectionMVVC) 35 | contentView.addSubview(myCollectionMVVC.view) 36 | myCollectionMVVC.view.snp.makeConstraints { make in 37 | make.edges.equalToSuperview() 38 | } 39 | 40 | myCollectionPlaylistVC.view.isHidden = false 41 | myCollectionAlbumVC.view.isHidden = true 42 | myCollectionPodcastVC.view.isHidden = true 43 | myCollectionMVVC.view.isHidden = true 44 | } 45 | 46 | } 47 | 48 | extension WKMyCollectionVC: UITabBarDelegate { 49 | func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { 50 | print(item) 51 | if item.title == "歌单" { 52 | myCollectionPlaylistVC.view.isHidden = false 53 | myCollectionAlbumVC.view.isHidden = true 54 | myCollectionPodcastVC.view.isHidden = true 55 | myCollectionMVVC.view.isHidden = true 56 | } else if item.title == "专辑" { 57 | myCollectionPlaylistVC.view.isHidden = true 58 | myCollectionAlbumVC.view.isHidden = false 59 | myCollectionPodcastVC.view.isHidden = true 60 | myCollectionMVVC.view.isHidden = true 61 | } else if item.title == "播客" { 62 | myCollectionPlaylistVC.view.isHidden = true 63 | myCollectionAlbumVC.view.isHidden = true 64 | myCollectionPodcastVC.view.isHidden = false 65 | myCollectionMVVC.view.isHidden = true 66 | } else if item.title == "MV" { 67 | myCollectionPlaylistVC.view.isHidden = true 68 | myCollectionAlbumVC.view.isHidden = true 69 | myCollectionPodcastVC.view.isHidden = true 70 | myCollectionMVVC.view.isHidden = false 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKMyPlaylistViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import NeteaseRequest 4 | class WKMyPlaylistViewController: UIViewController { 5 | 6 | @IBOutlet weak var collectionView: UICollectionView! 7 | var playList = [NRPlayListModel]() 8 | 9 | static func creat() -> WKMyPlaylistViewController { 10 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKMyPlaylistViewController 11 | return vc 12 | } 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | collectionView.register(WKPlayListCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self)) 18 | collectionView.collectionViewLayout = makeRencentPlaylistCollectionViewLayout() 19 | Task { 20 | await loadData() 21 | } 22 | } 23 | 24 | func loadData() async { 25 | do { 26 | if let userModel: NRProfileModel = UserDefaults.standard.codable(forKey: "userModel") { 27 | playList = try await fetchUserPlaylist(cookie: cookie, uid: userModel.userId, limit: 1000).filter { model in 28 | model.userId == userModel.userId 29 | } 30 | collectionView.reloadData() 31 | } 32 | } catch { 33 | print(error) 34 | } 35 | } 36 | 37 | 38 | } 39 | 40 | extension WKMyPlaylistViewController { 41 | 42 | func makeRencentPlaylistCollectionViewLayout () -> UICollectionViewLayout { 43 | UICollectionViewCompositionalLayout { 44 | [weak self] _, _ in 45 | return self?.makeGridLayoutSection() 46 | } 47 | } 48 | 49 | 50 | func makeGridLayoutSection() -> NSCollectionLayoutSection { 51 | 52 | // let style = styleOverride ?? Settings.displayStyle 53 | // let heightDimension = NSCollectionLayoutDimension.estimated(380) 54 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 55 | widthDimension: .fractionalWidth(1/3), 56 | heightDimension: .fractionalHeight(1) 57 | )) 58 | let hSpacing: CGFloat = 30 59 | item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: hSpacing, bottom: 0, trailing: hSpacing) 60 | let group = NSCollectionLayoutGroup.horizontalGroup(with: NSCollectionLayoutSize( 61 | widthDimension: .fractionalWidth(1), 62 | heightDimension:.fractionalWidth(1/3) 63 | ), repeatingSubitem: item, count: 3) 64 | let vSpacing: CGFloat = 16 65 | let baseSpacing: CGFloat = 24 66 | group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(baseSpacing), top: .fixed(vSpacing), trailing: .fixed(0), bottom: .fixed(vSpacing)) 67 | let section = NSCollectionLayoutSection(group: group) 68 | if baseSpacing > 0 { 69 | section.contentInsets = NSDirectionalEdgeInsets(top: baseSpacing, leading: 0, bottom: 0, trailing: 0) 70 | } 71 | return section 72 | } 73 | } 74 | 75 | extension WKMyPlaylistViewController: UICollectionViewDelegate, UICollectionViewDataSource { 76 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 77 | return playList.count 78 | } 79 | 80 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 81 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self), for: indexPath) as! WKPlayListCollectionViewCell 82 | cell.playListCover.kf.setImage(with: URL(string: playList[indexPath.row].coverImgUrl!)) 83 | cell.titleLabel.text = playList[indexPath.row].name 84 | return cell 85 | } 86 | 87 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 88 | let playListDetaiVC = WKPlayListDetailViewController.creat(playListId: playList[indexPath.row].id) 89 | playListDetaiVC.modalPresentationStyle = .blurOverFullScreen 90 | self.present(playListDetaiVC, animated: true) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKProfileHeader.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import MarqueeLabel 3 | class WKProfileHeader: UITableViewHeaderFooterView { 4 | var avatarView = WKAvatarView() 5 | var nameLabel = MarqueeLabel() 6 | var followedsLabel = UILabel() 7 | var followsLabel = UILabel() 8 | var levelLabel = UILabel() 9 | var signatureView = WKDescView() 10 | var clickAvatarAction: (() -> Void)? 11 | override init(reuseIdentifier: String?) { 12 | super.init(reuseIdentifier: reuseIdentifier) 13 | setup() 14 | } 15 | 16 | required init?(coder: NSCoder) { 17 | fatalError("init(coder:) has not been implemented") 18 | } 19 | 20 | func setup() { 21 | ({(view: WKAvatarView) in 22 | view.onPrimaryAction = { [weak self] view in 23 | self!.clickAvatarAction!() 24 | } 25 | self.contentView.addSubview(view) 26 | view.snp.makeConstraints { make in 27 | make.centerX.equalToSuperview() 28 | make.top.equalToSuperview() 29 | make.size.equalTo(200) 30 | } 31 | })(avatarView) 32 | 33 | ({(label: MarqueeLabel) in 34 | label.textAlignment = .center 35 | self.contentView.addSubview(label) 36 | label.snp.makeConstraints { make in 37 | make.left.equalTo(self.contentView).offset(10) 38 | make.right.equalTo(self.contentView).offset(-10) 39 | make.top.equalTo(self.avatarView.snp.bottom).offset(10) 40 | } 41 | })(nameLabel) 42 | 43 | ({(label: UILabel) in 44 | label.font = .systemFont(ofSize: 25, weight: .semibold) 45 | self.contentView.addSubview(label) 46 | label.snp.makeConstraints { make in 47 | make.centerX.equalToSuperview() 48 | make.top.equalTo(nameLabel.snp.bottom).offset(10) 49 | } 50 | })(followedsLabel) 51 | 52 | ({(label: UILabel) in 53 | label.font = .systemFont(ofSize: 25, weight: .semibold) 54 | self.contentView.addSubview(label) 55 | label.snp.makeConstraints { make in 56 | make.right.equalTo(followedsLabel.snp.left).offset(-10) 57 | make.centerY.equalTo(followedsLabel) 58 | } 59 | })(followsLabel) 60 | 61 | ({(label: UILabel) in 62 | label.font = .systemFont(ofSize: 25, weight: .semibold) 63 | self.contentView.addSubview(label) 64 | label.snp.makeConstraints { make in 65 | make.left.equalTo(followedsLabel.snp.right).offset(10) 66 | make.centerY.equalTo(followedsLabel) 67 | } 68 | })(levelLabel) 69 | 70 | ({(view: WKDescView) in 71 | self.contentView.addSubview(view) 72 | view.snp.makeConstraints { make in 73 | make.left.equalTo(contentView).offset(10) 74 | make.right.equalTo(contentView).offset(-10) 75 | make.top.equalTo((followedsLabel.snp.bottom)).offset(10) 76 | make.height.equalTo(100) 77 | } 78 | })(signatureView) 79 | } 80 | 81 | 82 | } 83 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKRecentAlbumListVC.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import NeteaseRequest 3 | class WKRecentAlbumListVC: UIViewController { 4 | 5 | @IBOutlet weak var collectionView: UICollectionView! 6 | fileprivate var albumModels = [NRAlbumModel]() 7 | static func creat() -> WKRecentAlbumListVC { 8 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKRecentAlbumListVC 9 | return vc 10 | } 11 | override func viewDidLoad() { 12 | super.viewDidLoad() 13 | collectionView.register(WKPlayListCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self)) 14 | collectionView.collectionViewLayout = makeRencentPlaylistCollectionViewLayout() 15 | Task { 16 | await loadData() 17 | } 18 | } 19 | 20 | func loadData() async { 21 | do { 22 | let recentModel: NRRecentPlayModel = try await fetchRecentAlbum(cookie: cookie, limit: 1000) 23 | albumModels = recentModel.list.map { $0.data } 24 | collectionView.reloadData() 25 | } catch { 26 | print(error) 27 | } 28 | } 29 | 30 | } 31 | 32 | extension WKRecentAlbumListVC: UICollectionViewDelegate, UICollectionViewDataSource { 33 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 34 | return albumModels.count 35 | } 36 | 37 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 38 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self), for: indexPath) as! WKPlayListCollectionViewCell 39 | cell.playListCover.kf.setImage(with: URL(string: albumModels[indexPath.row].picUrl!)) 40 | cell.titleLabel.text = albumModels[indexPath.row].name 41 | return cell 42 | } 43 | 44 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 45 | let albumVC = WKAlbumDetailViewController.creat(playListId: (albumModels[indexPath.row].id)!) 46 | albumVC.modalPresentationStyle = .blurOverFullScreen 47 | self.present(albumVC, animated: true) 48 | } 49 | } 50 | 51 | 52 | extension WKRecentAlbumListVC { 53 | func makeRencentPlaylistCollectionViewLayout () -> UICollectionViewLayout { 54 | UICollectionViewCompositionalLayout { 55 | [weak self] _, _ in 56 | return self?.makeGridLayoutSection() 57 | } 58 | } 59 | 60 | 61 | func makeGridLayoutSection() -> NSCollectionLayoutSection { 62 | 63 | // let style = styleOverride ?? Settings.displayStyle 64 | // let heightDimension = NSCollectionLayoutDimension.estimated(380) 65 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 66 | widthDimension: .fractionalWidth(1/3), 67 | heightDimension: .fractionalHeight(1) 68 | )) 69 | let hSpacing: CGFloat = 30 70 | item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: hSpacing, bottom: 0, trailing: hSpacing) 71 | let group = NSCollectionLayoutGroup.horizontalGroup(with: NSCollectionLayoutSize( 72 | widthDimension: .fractionalWidth(1), 73 | heightDimension:.fractionalWidth(1/3) 74 | ), repeatingSubitem: item, count: 3) 75 | let vSpacing: CGFloat = 16 76 | let baseSpacing: CGFloat = 24 77 | group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(baseSpacing), top: .fixed(vSpacing), trailing: .fixed(0), bottom: .fixed(vSpacing)) 78 | let section = NSCollectionLayoutSection(group: group) 79 | if baseSpacing > 0 { 80 | section.contentInsets = NSDirectionalEdgeInsets(top: baseSpacing, leading: 0, bottom: 0, trailing: 0) 81 | } 82 | return section 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKRecentPodcastListVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import UIKit 4 | import NeteaseRequest 5 | class WKRecentPodcastListVC: UIViewController { 6 | @IBOutlet weak var collectionView: UICollectionView! 7 | fileprivate var podcastModels = [NRDJRadioModel]() 8 | static func creat() -> WKRecentPodcastListVC { 9 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKRecentPodcastListVC 10 | return vc 11 | } 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | collectionView.register(WKPlayListCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self)) 16 | collectionView.collectionViewLayout = makeRecentPodcastCollectionViewLayout() 17 | Task { 18 | await loadData() 19 | } 20 | } 21 | 22 | func loadData() async { 23 | do { 24 | let recentModel: NRRecentPlayModel = try await fetchRecentDj(cookie: cookie, limit: 1000) 25 | podcastModels = recentModel.list.map { $0.data } 26 | collectionView.reloadData() 27 | } catch { 28 | 29 | } 30 | } 31 | 32 | } 33 | 34 | extension WKRecentPodcastListVC: UICollectionViewDelegate, UICollectionViewDataSource { 35 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 36 | return podcastModels.count 37 | } 38 | 39 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 40 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: WKPlayListCollectionViewCell.self), for: indexPath) as! WKPlayListCollectionViewCell 41 | cell.playListCover.kf.setImage(with: URL(string: podcastModels[indexPath.row].picUrl)) 42 | cell.titleLabel.text = podcastModels[indexPath.row].name 43 | return cell 44 | } 45 | 46 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 47 | let playListDetaiVC = WKPodcastDetailViewController.creat(djRadioModel: podcastModels[indexPath.row]) 48 | playListDetaiVC.modalPresentationStyle = .blurOverFullScreen 49 | self.present(playListDetaiVC, animated: true) 50 | } 51 | } 52 | 53 | extension WKRecentPodcastListVC { 54 | 55 | func makeRecentPodcastCollectionViewLayout () -> UICollectionViewLayout { 56 | UICollectionViewCompositionalLayout { 57 | [weak self] _, _ in 58 | return self?.makeGridLayoutSection() 59 | } 60 | } 61 | 62 | 63 | func makeGridLayoutSection() -> NSCollectionLayoutSection { 64 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 65 | widthDimension: .fractionalWidth(1/3), 66 | heightDimension: .fractionalHeight(1) 67 | )) 68 | let hSpacing: CGFloat = 30 69 | item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: hSpacing, bottom: 0, trailing: hSpacing) 70 | let group = NSCollectionLayoutGroup.horizontalGroup(with: NSCollectionLayoutSize( 71 | widthDimension: .fractionalWidth(1), 72 | heightDimension:.fractionalWidth(1/3) 73 | ), repeatingSubitem: item, count: 3) 74 | let vSpacing: CGFloat = 16 75 | let baseSpacing: CGFloat = 24 76 | group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(baseSpacing), top: .fixed(vSpacing), trailing: .fixed(0), bottom: .fixed(vSpacing)) 77 | let section = NSCollectionLayoutSection(group: group) 78 | if baseSpacing > 0 { 79 | section.contentInsets = NSDirectionalEdgeInsets(top: baseSpacing, leading: 0, bottom: 0, trailing: 0) 80 | } 81 | return section 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKRecentSongListVC.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import NeteaseRequest 3 | class WKRecentSongListVC: UIViewController { 4 | 5 | @IBOutlet weak var tableView: UITableView! 6 | private var allModels: [CustomAudioModel] = [CustomAudioModel]() 7 | 8 | static func creat() -> WKRecentSongListVC { 9 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKRecentSongListVC 10 | return vc 11 | } 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | tableView.register(WKSongTableViewCell.self, forCellReuseIdentifier: "WKSongTableViewCell") 15 | Task { 16 | await loadData() 17 | self.tableView.reloadData() 18 | } 19 | } 20 | 21 | func loadData() async { 22 | do { 23 | let recentModel: NRRecentPlayModel = try await fetchRecentSong(cookie: cookie,limit: 1000) 24 | self.allModels.removeAll() 25 | for resourceModel in recentModel.list { 26 | let model = CustomAudioModel() 27 | model.audioId = resourceModel.data.id 28 | model.like = likeIds.contains(resourceModel.data.id) 29 | model.isFree = 1 30 | // model.fee = resourceModel.data.fee 31 | model.freeTime = 0 32 | model.audioTitle = resourceModel.data.name 33 | model.audioPicUrl = resourceModel.data.al?.picUrl 34 | model.transTitle = resourceModel.data.tns?.first 35 | model.albumTitle = resourceModel.data.al?.name 36 | let min = (resourceModel.data.dt ?? 0) / 1000 / 60 37 | let sec = (resourceModel.data.dt ?? 0) / 1000 % 60 38 | model.audioTime = String(format: "%d:%02d", min, sec) 39 | if let singerModel = resourceModel.data.ar { 40 | model.singer = singerModel.map { $0.name ?? "" }.joined(separator: "/") 41 | } 42 | self.allModels.append(model) 43 | } 44 | 45 | } catch { 46 | print(error) 47 | } 48 | } 49 | 50 | } 51 | 52 | extension WKRecentSongListVC: UITableViewDelegate, UITableViewDataSource { 53 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 54 | return allModels.count 55 | } 56 | 57 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 58 | let cell = tableView.dequeueReusableCell(withIdentifier: "WKSongTableViewCell", for: indexPath) as! WKSongTableViewCell 59 | cell.indexLabel.text = "\(indexPath.row + 1)" + "." 60 | cell.setModel(allModels[indexPath.row]) 61 | cell.albumLabel.text = (allModels[indexPath.row].singer ?? "") + " - " + (allModels[indexPath.row].albumTitle ?? "") 62 | return cell 63 | } 64 | 65 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 66 | if wk_player.isPlaying && wk_player.currentModel?.audioId == self.allModels[indexPath.row].audioId { 67 | let playingVC = WKPlayingViewController.creat() 68 | self.present(playingVC, animated: true) 69 | return 70 | } 71 | wk_player.allOriginalModels = self.allModels 72 | try? wk_player.play(index: indexPath.row) 73 | let playingVC = WKPlayingViewController.creat() 74 | self.present(playingVC, animated: true) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKRecentVideoVC.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import NeteaseRequest 4 | 5 | class WKRecentVideoVC: UIViewController { 6 | 7 | @IBOutlet weak var collectionView: UICollectionView! 8 | private var videoList = [NRVideoModel]() 9 | static func creat() -> WKRecentVideoVC { 10 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKRecentVideoVC 11 | return vc 12 | } 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | collectionView.register(WKMVCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: WKMVCollectionViewCell.self)) 16 | collectionView.collectionViewLayout = makeVideoCollectionViewLayout() 17 | Task { 18 | await loadData() 19 | } 20 | } 21 | 22 | func loadData() async { 23 | do { 24 | let recentModel: NRRecentPlayModel = try await fetchRecentVideo(cookie: cookie, limit: 1000) 25 | videoList = recentModel.list.map { $0.data } 26 | collectionView.reloadData() 27 | } catch { 28 | print(error) 29 | } 30 | } 31 | 32 | func makeVideoCollectionViewLayout () -> UICollectionViewLayout { 33 | UICollectionViewCompositionalLayout { 34 | [weak self] _, _ in 35 | return self?.makeGridLayoutSection() 36 | } 37 | } 38 | 39 | func makeGridLayoutSection() -> NSCollectionLayoutSection { 40 | 41 | // let style = styleOverride ?? Settings.displayStyle 42 | // let heightDimension = NSCollectionLayoutDimension.estimated(380) 43 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 44 | widthDimension: .fractionalWidth(1/3), 45 | heightDimension: .fractionalHeight(1) 46 | )) 47 | let hSpacing: CGFloat = 30 48 | item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: hSpacing, bottom: 0, trailing: hSpacing) 49 | let group = NSCollectionLayoutGroup.horizontalGroup(with: NSCollectionLayoutSize( 50 | widthDimension: .fractionalWidth(1), 51 | heightDimension:.fractionalWidth(0.2) 52 | ), repeatingSubitem: item, count: 3) 53 | let vSpacing: CGFloat = 16 54 | let baseSpacing: CGFloat = 24 55 | group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(baseSpacing), top: .fixed(vSpacing), trailing: .fixed(0), bottom: .fixed(vSpacing)) 56 | let section = NSCollectionLayoutSection(group: group) 57 | if baseSpacing > 0 { 58 | section.contentInsets = NSDirectionalEdgeInsets(top: baseSpacing, leading: 0, bottom: 0, trailing: hSpacing) 59 | } 60 | return section 61 | } 62 | 63 | } 64 | 65 | extension WKRecentVideoVC: UICollectionViewDelegate, UICollectionViewDataSource { 66 | 67 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 68 | return videoList.count 69 | } 70 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 71 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: WKMVCollectionViewCell.self), for: indexPath) as! WKMVCollectionViewCell 72 | cell.coverImageView.kf.setImage(with: URL(string: videoList[indexPath.row].coverUrl ?? "")) 73 | cell.titleLabel.text = videoList[indexPath.row].title 74 | let min = videoList[indexPath.row].duration! / 1000 / 60 75 | let sec = videoList[indexPath.row].duration! / 1000 % 60 76 | cell.duartionLabel.text = String(format: "%02d:%02d", min, sec) 77 | return cell 78 | } 79 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 80 | let videoPlayerVC = WKVideoViewController(playInfo: WKPlayInfo(id: Int(videoList[indexPath.row].vid!)!, r: 1080, isMV: true)) 81 | self.present(videoPlayerVC, animated: true) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKRencentVoiceListVC.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import NeteaseRequest 4 | class WKRencentVoiceListVC: UIViewController { 5 | @IBOutlet weak var tableView: UITableView! 6 | var allModels: [CustomAudioModel] = [CustomAudioModel]() 7 | static func creat() -> WKRencentVoiceListVC { 8 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKRencentVoiceListVC 9 | return vc 10 | } 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | tableView.register(WKPlayListTableViewCell.self, forCellReuseIdentifier: "WKPlayListTableViewCell") 15 | Task { 16 | await loadData() 17 | self.tableView.reloadData() 18 | } 19 | } 20 | 21 | func loadData() async { 22 | do { 23 | let rencentModel: NRRecentPlayModel = try await fetchRecentVoice(cookie: cookie, limit: 1000) 24 | for songModel in rencentModel.list { 25 | let model = CustomAudioModel() 26 | model.audioId = songModel.data.pubDJProgramData.mainTrackId 27 | model.isFree = 1 28 | model.freeTime = 0 29 | model.audioTitle = songModel.data.pubDJProgramData.name 30 | model.audioPicUrl = songModel.data.pubDJProgramData.coverUrl 31 | let min = songModel.data.pubDJProgramData.duration / 1000 / 60 32 | let sec = songModel.data.pubDJProgramData.duration / 1000 % 60 33 | model.audioTime = String(format: "%d:%02d", min, sec) 34 | model.singer = songModel.data.pubDJProgramData.dj.nickname 35 | self.allModels.append(model) 36 | } 37 | } catch { 38 | print(error) 39 | } 40 | } 41 | 42 | } 43 | 44 | extension WKRencentVoiceListVC: UITableViewDelegate, UITableViewDataSource { 45 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 46 | return allModels.count 47 | } 48 | 49 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 50 | let cell = tableView.dequeueReusableCell(withIdentifier: "WKPlayListTableViewCell", for: indexPath) as! WKPlayListTableViewCell 51 | cell.setModel(allModels[indexPath.row]) 52 | return cell 53 | } 54 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 55 | if wk_player.isPlaying && wk_player.currentModel?.audioId == self.allModels[indexPath.row].audioId { 56 | let playingVC = WKPlayingViewController.creat() 57 | self.present(playingVC, animated: true) 58 | return 59 | } 60 | let model: [CustomAudioModel] = [self.allModels[indexPath.row]] 61 | wk_player.allOriginalModels = model 62 | try? wk_player.play(index: 0) 63 | let playingVC = WKPlayingViewController.creat(isPodcast: true) 64 | self.present(playingVC, animated: true) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKSettingViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import NeteaseRequest 4 | struct CellModel { 5 | let title: String 6 | var desp: String? = nil 7 | var contentVC: UIViewController? = nil 8 | var action: (() -> Void)? = nil 9 | } 10 | 11 | class WKSettingViewController: UIViewController { 12 | @IBOutlet weak var tableView: UITableView! 13 | var cellModels = [CellModel]() 14 | static func creat() -> WKSettingViewController { 15 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKSettingViewController 16 | return vc 17 | } 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 21 | setupData() 22 | } 23 | 24 | func setupData() { 25 | cellModels.removeAll() 26 | let cancelAction = UIAlertAction(title: nil, style: .cancel) 27 | let quality = CellModel(title: "默认音质", desp: Settings.audioQuality.desp) { [weak self] in 28 | let alert = UIAlertController(title: "默认音质", message: "标准以上需要网易云音乐会员", preferredStyle: .actionSheet) 29 | for quality in NRSongLevel.allCases { 30 | let action = UIAlertAction(title: quality.desp, style: .default) { _ in 31 | Settings.audioQuality = quality 32 | self?.setupData() 33 | } 34 | alert.addAction(action) 35 | } 36 | alert.addAction(cancelAction) 37 | self?.present(alert, animated: true) 38 | } 39 | cellModels.append(quality) 40 | let comment = CellModel(title: "热门评论", desp: Settings.hotComment ? "开" : "关") { [weak self] in 41 | Settings.hotComment = !Settings.hotComment 42 | self?.setupData() 43 | } 44 | cellModels.append(comment) 45 | 46 | let fluidBg = CellModel(title: "流体背景", desp: Settings.fluidBg ? "开" : "关") { [weak self] in 47 | Settings.fluidBg = !Settings.fluidBg 48 | self?.setupData() 49 | } 50 | cellModels.append(fluidBg) 51 | 52 | cellModels.append(quality) 53 | // let topShelf = CellModel(title: "TopShelf", desp: "正在播放") { [weak self] in 54 | // 55 | // } 56 | // cellModels.append(topShelf) 57 | 58 | let service = CellModel(title: "服务地址", desp: Settings.service) { [weak self] in 59 | let vc = WKInputViewController.creat() 60 | vc.modalPresentationStyle = .blurOverFullScreen 61 | self?.present(vc, animated: true) 62 | } 63 | cellModels.append(service) 64 | 65 | tableView.reloadData() 66 | } 67 | } 68 | 69 | 70 | 71 | 72 | extension WKSettingViewController: UITableViewDelegate, UITableViewDataSource { 73 | func numberOfSections(in tableView: UITableView) -> Int { 74 | return 1 75 | } 76 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 77 | return cellModels.count 78 | } 79 | 80 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 81 | let cell = UITableViewCell(style: .value1, reuseIdentifier: "cell") 82 | // cell.accessoryType = .disclosureIndicator 83 | var content = cell.defaultContentConfiguration() 84 | content.text = cellModels[indexPath.row].title 85 | content.secondaryText = cellModels[indexPath.row].desp 86 | cell.contentConfiguration = content 87 | return cell 88 | } 89 | 90 | 91 | 92 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 93 | cellModels[indexPath.row].action?() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Profile/WKSwitchAccountVC.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import Kingfisher 4 | class WKSwitchAccountVC: UIViewController { 5 | 6 | @IBOutlet weak var collectionView: UICollectionView! 7 | var accountModels: [WKUserModel] { 8 | if let am:[WKUserModel] = UserDefaults.standard.codable(forKey: "accounts") { 9 | return am 10 | } 11 | return [] 12 | } 13 | static func creat() -> WKSwitchAccountVC { 14 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKSwitchAccountVC 15 | return vc 16 | } 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | collectionView.register(WKAccountCell.self, forCellWithReuseIdentifier: String(describing: WKAccountCell.self)) 21 | collectionView.collectionViewLayout = makeSwitchAccountLayout() 22 | } 23 | 24 | func makeSwitchAccountLayout () -> UICollectionViewLayout { 25 | 26 | UICollectionViewCompositionalLayout { 27 | [weak self] _, _ in 28 | return self?.makeGridLayoutSection() 29 | } 30 | 31 | 32 | } 33 | 34 | func makeGridLayoutSection() -> NSCollectionLayoutSection { 35 | 36 | // let style = styleOverride ?? Settings.displayStyle 37 | // let heightDimension = NSCollectionLayoutDimension.estimated(380) 38 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 39 | widthDimension: .fractionalWidth(1/5), 40 | heightDimension: .fractionalHeight(1) 41 | )) 42 | let hSpacing: CGFloat = 30 43 | item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: hSpacing, bottom: 0, trailing: hSpacing) 44 | let group = NSCollectionLayoutGroup.horizontalGroup(with: NSCollectionLayoutSize( 45 | widthDimension: .fractionalWidth(1), 46 | heightDimension:.fractionalHeight(1) 47 | ), repeatingSubitem: item, count: 5) 48 | let vSpacing: CGFloat = 0 49 | let baseSpacing: CGFloat = 0 50 | group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(baseSpacing), top: .fixed(vSpacing), trailing: .fixed(0), bottom: .fixed(vSpacing)) 51 | let section = NSCollectionLayoutSection(group: group) 52 | section.interGroupSpacing = 0 53 | return section 54 | } 55 | 56 | 57 | } 58 | 59 | extension WKSwitchAccountVC: UICollectionViewDelegate, UICollectionViewDataSource { 60 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 61 | return accountModels.count + 1 62 | } 63 | 64 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 65 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: WKAccountCell.self), for: indexPath) as! WKAccountCell 66 | if indexPath.row == accountModels.count { 67 | cell.userView.image = UIImage(named: "add_account") 68 | cell.userView.title = "" 69 | } else { 70 | KingfisherManager.shared.retrieveImage(with: URL(string: accountModels[indexPath.row].user.avatarUrl)!, options: nil, progressBlock: nil) { result in 71 | switch result { 72 | case .success(let value): 73 | cell.userView.image = value.image 74 | case .failure(_): 75 | debugPrint("下载图片失败") 76 | } 77 | } 78 | cell.userView.title = accountModels[indexPath.row].user.nickname 79 | } 80 | // cell.loadData(with: nil, isAdd: indexPath.row == accountModels.count) 81 | return cell 82 | } 83 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 84 | if indexPath.row == accountModels.count { 85 | let loginVC = WKLoginViewController.creat() 86 | loginVC.modalPresentationStyle = .blurOverFullScreen 87 | self.present(loginVC, animated: true) 88 | } else { 89 | UserDefaults.standard.setValue(accountModels[indexPath.row].cookie, forKey: "cookie") 90 | cookie = accountModels[indexPath.row].cookie 91 | NotificationCenter.default.post(name: NSNotification.Name(rawValue: "login"), object: nil, userInfo: nil) 92 | AppDelegate.shared.showTabBar() 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Recommend/Banner/FSPagerCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPagerCollectionView.swift 3 | // FSPagerView 4 | // 5 | // Created by Wenchao Ding on 24/12/2016. 6 | // Copyright © 2016 Wenchao Ding. All rights reserved. 7 | // 8 | // 1. Reject -[UIScrollView(UIScrollViewInternal) _adjustContentOffsetIfNecessary] 9 | // 2. Group initialized features 10 | 11 | import UIKit 12 | 13 | class FSPagerCollectionView: UICollectionView { 14 | 15 | #if !os(tvOS) 16 | override var scrollsToTop: Bool { 17 | set { 18 | super.scrollsToTop = false 19 | } 20 | get { 21 | return false 22 | } 23 | } 24 | #endif 25 | 26 | override var contentInset: UIEdgeInsets { 27 | set { 28 | super.contentInset = .zero 29 | if (newValue.top > 0) { 30 | let contentOffset = CGPoint(x:self.contentOffset.x, y:self.contentOffset.y+newValue.top); 31 | self.contentOffset = contentOffset 32 | } 33 | } 34 | get { 35 | return super.contentInset 36 | } 37 | } 38 | 39 | override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { 40 | super.init(frame: frame, collectionViewLayout: layout) 41 | self.commonInit() 42 | } 43 | 44 | required init?(coder aDecoder: NSCoder) { 45 | super.init(coder: aDecoder) 46 | self.commonInit() 47 | } 48 | 49 | fileprivate func commonInit() { 50 | self.contentInset = .zero 51 | self.decelerationRate = UIScrollView.DecelerationRate.fast 52 | self.showsVerticalScrollIndicator = false 53 | self.showsHorizontalScrollIndicator = false 54 | if #available(iOS 10.0, *) { 55 | self.isPrefetchingEnabled = false 56 | } 57 | if #available(iOS 11.0, *) { 58 | self.contentInsetAdjustmentBehavior = .never 59 | } 60 | #if !os(tvOS) 61 | self.scrollsToTop = false 62 | self.isPagingEnabled = false 63 | #endif 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Recommend/Banner/FSPagerViewLayoutAttributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSPagerViewLayoutAttributes.swift 3 | // FSPagerViewExample 4 | // 5 | // Created by Wenchao Ding on 26/02/2017. 6 | // Copyright © 2017 Wenchao Ding. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class FSPagerViewLayoutAttributes: UICollectionViewLayoutAttributes { 12 | 13 | open var position: CGFloat = 0 14 | 15 | open override func isEqual(_ object: Any?) -> Bool { 16 | guard let object = object as? FSPagerViewLayoutAttributes else { 17 | return false 18 | } 19 | var isEqual = super.isEqual(object) 20 | isEqual = isEqual && (self.position == object.position) 21 | return isEqual 22 | } 23 | 24 | open override func copy(with zone: NSZone? = nil) -> Any { 25 | let copy = super.copy(with: zone) as! FSPagerViewLayoutAttributes 26 | copy.position = self.position 27 | return copy 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Recommend/Banner/FSPagerViewObjcCompat.h: -------------------------------------------------------------------------------- 1 | // 2 | // FSPagerViewObjcCompat.h 3 | // FSPagerView 4 | // 5 | // Created by 丁文超 on 2018/9/18. 6 | // Copyright © 2018 Wenchao Ding. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | #define FSPagerViewExtern extern 13 | 14 | /** 15 | Requests that FSPagerView use the default value for a given distance. 16 | */ 17 | FSPagerViewExtern NSUInteger const FSPagerViewAutomaticDistance; 18 | 19 | /** 20 | Requests that FSPagerView use the default value for a given size. 21 | */ 22 | FSPagerViewExtern CGSize const FSPagerViewAutomaticSize; 23 | 24 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Recommend/Banner/FSPagerViewObjcCompat.m: -------------------------------------------------------------------------------- 1 | // 2 | // FSPagerViewObjcCompat.m 3 | // FSPagerView 4 | // 5 | // Created by Wenchao Ding on 2018/9/24. 6 | // Copyright © 2018 Wenchao Ding. All rights reserved. 7 | // 8 | 9 | #import "FSPagerViewObjcCompat.h" 10 | 11 | NSUInteger const FSPagerViewAutomaticDistance = 0; 12 | CGSize const FSPagerViewAutomaticSize = { .width = 0, .height = 0 }; 13 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Recommend/WKSingleSongView.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import MarqueeLabel 4 | import NeteaseRequest 5 | class WKSingleSongView: UIControl { 6 | private var picView = UIImageView () 7 | private var songNameLabel = MarqueeLabel() 8 | private var singerLabel = MarqueeLabel() 9 | var audioModel = CustomAudioModel() 10 | var onPrimaryAction: ((CustomAudioModel) -> Void)? 11 | private let backgroundView = UIView() 12 | init() { 13 | super.init(frame: .zero) 14 | setupViews() 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | super.init(coder: coder) 19 | setupViews() 20 | } 21 | 22 | func setupViews() { 23 | 24 | ({(view: UIView) in 25 | view.backgroundColor = UIColor(white: 0.9, alpha: 0.4) 26 | view.layer.shadowOffset = CGSizeMake(0, 10) 27 | view.layer.shadowOpacity = 0.15 28 | view.layer.shadowRadius = 16.0 29 | view.layer.cornerRadius = 20 30 | view.layer.cornerCurve = .continuous 31 | view.isHidden = !isFocused 32 | addSubview(view) 33 | view.snp.makeConstraints { make in 34 | make.top.bottom.equalToSuperview() 35 | make.left.equalToSuperview() 36 | make.right.equalToSuperview() 37 | } 38 | })(backgroundView) 39 | 40 | ({(picView: UIImageView) in 41 | picView.contentMode = .scaleAspectFit 42 | picView.layer.cornerRadius = 10 43 | picView.clipsToBounds = true 44 | picView.layer.masksToBounds = true 45 | addSubview(picView) 46 | picView.snp.makeConstraints { make in 47 | make.left.equalToSuperview() 48 | make.top.bottom.equalToSuperview() 49 | make.size.equalTo(100) 50 | } 51 | })(picView) 52 | 53 | ({(label: MarqueeLabel) in 54 | label.textColor = UIColor(named: "titleColor") 55 | addSubview(label) 56 | label.snp.makeConstraints { make in 57 | make.left.equalTo(picView.snp.right).offset(20) 58 | make.right.equalToSuperview() 59 | make.bottom.equalTo(self.snp.centerY).offset(-5) 60 | } 61 | })(songNameLabel) 62 | 63 | ({(label: UILabel) in 64 | label.textColor = UIColor(named: "titleColor") 65 | label.font = .systemFont(ofSize: 30) 66 | addSubview(label) 67 | label.snp.makeConstraints { make in 68 | make.left.equalTo(picView.snp.right).offset(20) 69 | make.right.equalTo(self).offset(-30) 70 | make.top.equalTo(self.snp.centerY).offset(5) 71 | } 72 | })(singerLabel) 73 | } 74 | 75 | func setModel(audioModel: CustomAudioModel) { 76 | self.audioModel = audioModel 77 | self.picView.kf.setImage(with: URL(string: audioModel.wk_audioPic ?? "")) 78 | self.songNameLabel.text = audioModel.wk_sourceName 79 | self.singerLabel.text = audioModel.wk_singerName 80 | } 81 | 82 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 83 | super.didUpdateFocus(in: context, with: coordinator) 84 | 85 | if isFocused { 86 | coordinator.addCoordinatedAnimations { 87 | self.transform = CGAffineTransformMakeScale(1.1, 1.1) 88 | } 89 | } else { 90 | coordinator.addCoordinatedAnimations { 91 | self.transform = CGAffineTransformIdentity 92 | } 93 | } 94 | 95 | // backgroundView.isHidden = !isFocused 96 | } 97 | 98 | override func pressesEnded(_ presses: Set, with event: UIPressesEvent?) { 99 | super.pressesEnded(presses, with: event) 100 | if presses.first?.type == .select { 101 | sendActions(for: .primaryActionTriggered) 102 | onPrimaryAction?(audioModel) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Recommend/WKSongCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import MarqueeLabel 4 | class WKSongCollectionViewCell: UICollectionViewCell { 5 | private var picView = UIImageView () 6 | private var songNameLabel = MarqueeLabel() 7 | private var singerLabel = MarqueeLabel() 8 | var scaleFactor: CGFloat = 1.05 9 | 10 | override init(frame: CGRect) { 11 | super.init(frame: frame) 12 | self.layer.cornerRadius = 10 13 | setupViews() 14 | } 15 | 16 | required init?(coder: NSCoder) { 17 | super.init(coder: coder) 18 | setupViews() 19 | } 20 | 21 | func setupViews() { 22 | ({(picView: UIImageView) in 23 | picView.contentMode = .scaleAspectFit 24 | picView.layer.cornerRadius = 10 25 | picView.clipsToBounds = true 26 | picView.layer.masksToBounds = true 27 | addSubview(picView) 28 | picView.snp.makeConstraints { make in 29 | make.left.equalToSuperview() 30 | make.top.bottom.equalToSuperview() 31 | make.width.equalTo(picView.snp.height) 32 | } 33 | })(picView) 34 | 35 | ({(label: MarqueeLabel) in 36 | label.textColor = UIColor(named: "titleColor") 37 | label.text = "这是一段比较长的歌名儿,123456789abcdeFG" 38 | addSubview(label) 39 | label.snp.makeConstraints { make in 40 | make.left.equalTo(picView.snp.right).offset(20) 41 | make.right.equalToSuperview() 42 | make.bottom.equalTo(self.snp.centerY).offset(-5) 43 | } 44 | })(songNameLabel) 45 | 46 | ({(label: UILabel) in 47 | label.textColor = UIColor(named: "titleColor") 48 | label.font = .systemFont(ofSize: 30) 49 | label.text = "这是一段比较长的歌手名儿,123456789abcdeFG" 50 | addSubview(label) 51 | label.snp.makeConstraints { make in 52 | make.left.equalTo(picView.snp.right).offset(20) 53 | make.right.equalTo(self).offset(-30) 54 | make.top.equalTo(self.snp.centerY).offset(5) 55 | } 56 | })(singerLabel) 57 | } 58 | 59 | func loadData(with model: CustomAudioModel) { 60 | self.picView.kf.setImage(with: URL(string: model.wk_audioPic ?? "")) 61 | self.songNameLabel.text = model.wk_sourceName 62 | self.singerLabel.text = model.wk_singerName 63 | 64 | } 65 | 66 | 67 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 68 | super.didUpdateFocus(in: context, with: coordinator) 69 | if isFocused { 70 | let scaleFactor = self.scaleFactor 71 | coordinator.addCoordinatedAnimations { 72 | self.transform = CGAffineTransformMakeScale(scaleFactor, scaleFactor) 73 | let scaleDiff = (self.bounds.size.height * scaleFactor - self.bounds.size.height) / 2 74 | self.transform = CGAffineTransformTranslate(self.transform, 0, -scaleDiff) 75 | self.layer.shadowOffset = CGSizeMake(0, 16) 76 | self.layer.shadowOpacity = 0.2 77 | self.layer.shadowRadius = 18.0 78 | self.layer.cornerRadius = 10 79 | } 80 | } else { 81 | coordinator.addCoordinatedAnimations { 82 | self.transform = CGAffineTransformIdentity 83 | self.layer.shadowOpacity = 0 84 | self.layer.shadowOffset = CGSizeMake(0, 0) 85 | self.layer.cornerRadius = 10 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/RemoteControl/WKNowPlayingInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NowPlayingInfo.swift 3 | // NeteaseTVDemo 4 | // 5 | // Created by DLancerC on 2023/12/16. 6 | // 7 | 8 | import MediaPlayer 9 | 10 | 11 | class WKNowPlayingInfo { 12 | // Avoid multi-thread accessing crashs. 13 | private(set) var existed: LockDictionary = LockDictionary.init(value: [MPMediaItemPropertyMediaType: MPNowPlayingInfoMediaType.audio.rawValue]) 14 | 15 | private lazy var _queue: DispatchQueue = { 16 | .init(label: "com.wk.Vibefy.nowplayinginfo", qos: .userInitiated) 17 | }() 18 | } 19 | 20 | extension WKNowPlayingInfo { 21 | 22 | func update(work: @escaping (WKNowPlayingInfo) -> Void) { 23 | work(self) 24 | existed.read { [weak self] in self?.set(info: $0) } 25 | } 26 | 27 | func set(info: [String: Any]?) { 28 | _queue.async { MPNowPlayingInfoCenter.default().nowPlayingInfo = info } 29 | } 30 | 31 | func reset() { 32 | existed.write { $0 = [MPMediaItemPropertyMediaType: MPNowPlayingInfoMediaType.audio.rawValue] } 33 | _queue.async { MPNowPlayingInfoCenter.default().nowPlayingInfo = nil } 34 | } 35 | 36 | @discardableResult 37 | func title(_ title: String?) -> Self { 38 | existed.write { $0[MPMediaItemPropertyTitle] = title } 39 | return self 40 | } 41 | 42 | @discardableResult 43 | func artist(_ artist: String?) -> Self { 44 | existed.write { $0[MPMediaItemPropertyArtist] = artist } 45 | return self 46 | } 47 | 48 | @discardableResult 49 | func artwork(_ artwork: UIImage) -> Self { 50 | existed.write { 51 | $0[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: artwork.size) { _ in artwork } 52 | } 53 | return self 54 | } 55 | 56 | @discardableResult 57 | func rate(_ rate: Float) -> Self { 58 | existed.write { $0[MPNowPlayingInfoPropertyPlaybackRate] = rate } 59 | return self 60 | } 61 | 62 | @discardableResult 63 | func duration(_ duration: TimeInterval) -> Self { 64 | existed.write { $0[MPMediaItemPropertyPlaybackDuration] = duration } 65 | return self 66 | } 67 | 68 | @discardableResult 69 | func time(_ time: TimeInterval) -> Self { 70 | existed.write { $0[MPNowPlayingInfoPropertyElapsedPlaybackTime] = time } 71 | return self 72 | } 73 | 74 | @discardableResult 75 | func id(_ id: Int) -> Self { 76 | let idKey = "audioId" 77 | if let audioId = existed.read({ $0[idKey]}) { 78 | if audioId as! Int != id { 79 | reset() 80 | } 81 | } 82 | existed.write { $0[idKey] = id } 83 | return self 84 | } 85 | 86 | @discardableResult 87 | func extra(_ extra: [String: Any]?) -> Self { 88 | guard let extra else { return self } 89 | existed.write { $0.merge(extra) { $1 } } 90 | return self 91 | } 92 | } 93 | 94 | final class LockDictionary { 95 | private let lock = NSLock() 96 | private var value: [String: Any] 97 | init(value: [String: Any]) { 98 | self.value = value 99 | } 100 | 101 | var wrappedValue: [String: Any] { 102 | get { lock.around { value } } 103 | set { lock.around { value = newValue } } 104 | } 105 | 106 | var projectedValue: LockDictionary { self } 107 | 108 | init(wrappedValue: [String: Any]) { 109 | value = wrappedValue 110 | } 111 | 112 | func read(_ closure: ([String: Any]) -> U) -> U { 113 | lock.around { closure(value) } 114 | } 115 | 116 | func write(_ closure: (inout [String: Any]) -> U) -> U { 117 | lock.around { closure(&value) } 118 | } 119 | } 120 | 121 | protocol Lockable { 122 | func lock() 123 | func unlock() 124 | } 125 | 126 | extension Lockable { 127 | func around(_ closure: () -> T) -> T { 128 | lock(); defer { unlock() } 129 | return closure() 130 | } 131 | } 132 | 133 | extension NSLock: Lockable {} 134 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/RemoteControl/WKRemoteCommandManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WKRemoteControlCommandManager.swift 3 | // NeteaseTVDemo 4 | // 5 | // Created by DLancerC on 2023/12/23. 6 | // 7 | 8 | import Foundation 9 | import MediaPlayer 10 | 11 | class WKRemoteCommandManager { 12 | fileprivate let remoteCommandCenter = MPRemoteCommandCenter.shared() 13 | let player : WKPlayer 14 | 15 | init(player: WKPlayer) { 16 | self.player = player 17 | } 18 | 19 | func playTrackCommand() -> Self { 20 | remoteCommandCenter.playCommand.isEnabled = true 21 | remoteCommandCenter.playCommand.addTarget { event in 22 | if !wk_player.isPlaying { 23 | wk_player.resumePlayer() 24 | } 25 | return .success 26 | } 27 | return self 28 | } 29 | 30 | func pauseTrackCommand() -> Self { 31 | remoteCommandCenter.pauseCommand.isEnabled = true 32 | remoteCommandCenter.pauseCommand.addTarget { event in 33 | if wk_player.isPlaying { 34 | wk_player.pausePlayer() 35 | } 36 | return .success 37 | } 38 | return self 39 | } 40 | 41 | func nextTrackCommand() -> Self { 42 | remoteCommandCenter.nextTrackCommand.isEnabled = true 43 | remoteCommandCenter.nextTrackCommand.addTarget { event in 44 | do { 45 | try wk_player.playNext() 46 | } catch { 47 | debugPrint(error) 48 | } 49 | return .success 50 | } 51 | return self 52 | } 53 | 54 | func previousTrackCommand() -> Self { 55 | remoteCommandCenter.previousTrackCommand.isEnabled = true 56 | remoteCommandCenter.previousTrackCommand.addTarget { event in 57 | // 处理暂停操作的逻辑 58 | do { 59 | try wk_player.playLast() 60 | } catch { 61 | debugPrint(error) 62 | } 63 | return .success 64 | } 65 | return self 66 | } 67 | 68 | // func changePlaybackPositionTrackCommand() { 69 | // remoteCommandCenter.changePlaybackPositionCommand.isEnabled = true 70 | // remoteCommandCenter.changePlaybackPositionCommand.addTarget { event in 71 | // let seconds = (event as? MPChangePlaybackPositionCommandEvent)?.positionTime ?? 0 72 | // wk_player.prepareForSeek(to: Float(seconds) / Float(wk_player.totalTime)) 73 | // wk_player.resumePlayer() 74 | // return .success 75 | // } 76 | // } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Search/WKSearchTypeModel.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | struct WKSearchTypeModel { 5 | var isSelected: Bool 6 | var name: String 7 | var type: Int 8 | } 9 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Modules/Search/WKSearchViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import NeteaseRequest 4 | class WKSearchViewController: UIViewController { 5 | private let searchController: UISearchController 6 | private let searchContainerViewController: UISearchContainerViewController 7 | private let searchResultsController = WKSearchResultViewController.creat() 8 | init() { 9 | self.searchController = UISearchController(searchResultsController: WKSearchResultViewController.creat()) 10 | self.searchContainerViewController = UISearchContainerViewController(searchController: searchController) 11 | super.init(nibName: nil, bundle: nil) 12 | tabBarItem = UITabBarItem(tabBarSystemItem: .search, tag: 0) 13 | } 14 | 15 | required init?(coder: NSCoder) { 16 | fatalError("init(coder:) has not been implemented") 17 | } 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | addChild(searchContainerViewController) 21 | searchContainerViewController.view.frame = view.bounds 22 | view.addSubview(searchContainerViewController.view) 23 | searchContainerViewController.didMove(toParent: self) 24 | searchController.searchResultsUpdater = self 25 | 26 | } 27 | 28 | } 29 | 30 | extension WKSearchViewController: UISearchResultsUpdating { 31 | func updateSearchResults(for searchController: UISearchController) { 32 | if let searchText = searchController.searchBar.text { 33 | print(searchText) 34 | UserDefaults.standard.set(searchText, forKey: "searchText") 35 | self.searchResultsController.query = searchText 36 | self.searchResultsController.searchData() 37 | } 38 | } 39 | 40 | func updateSearchResults(for searchController: UISearchController, selecting searchSuggestion: UISearchSuggestion) { 41 | print(searchSuggestion.localizedSuggestion ?? "") 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /NeteaseTVDemo/NeteaseTVDemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.com.wk.Vibefy 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /NeteaseTVDemo/Settings.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NeteaseRequest 3 | 4 | 5 | enum Settings { 6 | @UserDefaultCodable("Settings.audioQuality", defaultValue: NRSongLevel.lossless) 7 | static var audioQuality: NRSongLevel 8 | 9 | @UserDefaultCodable("Settings.hotComment", defaultValue: true) 10 | static var hotComment: Bool 11 | // https://service-9ha5w8dk-1259615918.gz.tencentapigw.com.cn/release/ 12 | @UserDefaultCodable("Settings.service", defaultValue: "https://service-9ha5w8dk-1259615918.gz.tencentapigw.com.cn/release") 13 | static var service: String 14 | 15 | @UserDefaultCodable("Settings.fluidBg", defaultValue: true) 16 | static var fluidBg: Bool 17 | 18 | } 19 | 20 | extension NRSongLevel { 21 | var desp: String { 22 | switch self { 23 | case .standard: 24 | return "标准" 25 | case .higher: 26 | return "较高" 27 | case .exhigh: 28 | return "极高" 29 | case .lossless: 30 | return "无损" 31 | case .hires: 32 | return "Hi-Res" 33 | case .jyeffect: 34 | return "高清环绕声" 35 | case .sky: 36 | return "沉浸环绕声" 37 | case .jymaster: 38 | return "超清母带" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /NeteaseTVDemo/WKConfig.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NeteaseRequest 3 | 4 | var cookie = "" 5 | var shufflePlay = false 6 | var likeIds = [Int]() 7 | 8 | func fetchLikeIds() async { 9 | do { 10 | if let userModel: NRProfileModel = UserDefaults.standard.codable(forKey: "userModel") { 11 | likeIds = try await fetchLikeMusicList(uid: userModel.userId, cookie: cookie) 12 | } 13 | } catch { 14 | print(error) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /NeteaseTVDemo/WKInputViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import NeteaseRequest 3 | 4 | 5 | class WKInputViewController: UIViewController { 6 | @IBOutlet weak var serviceTextField: UITextField! 7 | static func creat() -> WKInputViewController { 8 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKInputViewController 9 | return vc 10 | } 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | self.serviceTextField.isSelected = false 15 | self.serviceTextField.placeholder = "https://" 16 | self.serviceTextField.text = Settings.service 17 | } 18 | 19 | @IBAction func doneButtonTaped(_ sender: Any) { 20 | guard let text = self.serviceTextField.text else { 21 | self.dismiss(animated: true) 22 | return 23 | } 24 | if text == Settings.service || text.count == 0 { 25 | self.dismiss(animated: true) 26 | return 27 | } 28 | let alert = UIAlertController.init(title: "修改服务地址", message: "确定要修改服务地址么,修改完成后需要重新启动软件", preferredStyle: .alert) 29 | let cancel = UIAlertAction.init(title: "取消", style: .cancel, handler: nil) 30 | alert.addAction(cancel) 31 | let confirm = UIAlertAction(title: "确定", style: .default) { _ in 32 | Settings.service = self.serviceTextField.text! 33 | abort() 34 | } 35 | alert.addAction(confirm) 36 | self.present(alert, animated: true, completion: nil) 37 | } 38 | } 39 | 40 | 41 | -------------------------------------------------------------------------------- /NeteaseTVDemo/WKMVCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import MarqueeLabel 3 | class WKMVCollectionViewCell: UICollectionViewCell { 4 | var coverImageView = UIImageView() 5 | var playCountLabel = UILabel() 6 | var duartionLabel = UILabel() 7 | var titleLabel = MarqueeLabel() 8 | var timeLabel = UILabel() 9 | 10 | override init(frame: CGRect) { 11 | super.init(frame: frame) 12 | ({(imageView: UIImageView) in 13 | imageView.layer.cornerRadius = 10 14 | imageView.clipsToBounds = true 15 | self.contentView.addSubview(imageView) 16 | imageView.snp.makeConstraints { make in 17 | make.edges.equalTo(UIEdgeInsets(top: 0, left: 0, bottom: 60, right: 0)) 18 | } 19 | })(coverImageView) 20 | 21 | ({(label: UILabel) in 22 | label.font = .systemFont(ofSize: 20) 23 | label.textAlignment = .left 24 | self.coverImageView.addSubview(label) 25 | label.snp.makeConstraints { make in 26 | make.bottom.equalTo(coverImageView).offset(-10) 27 | make.left.equalTo(coverImageView).offset(10) 28 | make.right.equalTo(coverImageView).offset(-10) 29 | } 30 | })(duartionLabel) 31 | 32 | ({(label: MarqueeLabel) in 33 | label.font = .systemFont(ofSize: 30) 34 | label.textAlignment = .center 35 | self.contentView.addSubview(label) 36 | label.snp.makeConstraints { make in 37 | make.top.equalTo(coverImageView.snp.bottom).offset(10) 38 | make.left.right.equalTo(self.contentView) 39 | } 40 | })(titleLabel) 41 | } 42 | 43 | required init?(coder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | 47 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 48 | super.didUpdateFocus(in: context, with: coordinator) 49 | if isFocused { 50 | coordinator.addCoordinatedAnimations { 51 | self.transform = CGAffineTransformMakeScale(1.1, 1.1) 52 | } 53 | } else { 54 | coordinator.addCoordinatedAnimations { 55 | self.transform = CGAffineTransformIdentity 56 | } 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /NeteaseTVDemo/WKPlayListCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import TVUIKit 4 | class WKPlayListCollectionViewCell: UICollectionViewCell { 5 | 6 | private var motionEffectV: UIInterpolatingMotionEffect! 7 | private var motionEffectH: UIInterpolatingMotionEffect! 8 | var playListCover: UIImageView = UIImageView() 9 | let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) 10 | var titleLabel: UILabel = UILabel() 11 | var posterView: TVPosterView! 12 | var scaleFactor: CGFloat = 1.05 13 | 14 | override init(frame: CGRect) { 15 | super.init(frame: frame) 16 | setupViews() 17 | } 18 | 19 | required init?(coder: NSCoder) { 20 | super.init(coder: coder) 21 | setupViews() 22 | } 23 | 24 | func setupViews() { 25 | self.clipsToBounds = true 26 | motionEffectV = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis) 27 | motionEffectV.maximumRelativeValue = 8 28 | motionEffectV.minimumRelativeValue = -8 29 | motionEffectH = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis) 30 | motionEffectH.maximumRelativeValue = 8 31 | motionEffectH.minimumRelativeValue = -8 32 | 33 | ({(imageView: UIImageView) in 34 | imageView.contentMode = .scaleAspectFill 35 | imageView.layer.cornerRadius = 10 36 | imageView.layer.masksToBounds = true 37 | imageView.clipsToBounds = true 38 | addSubview(imageView) 39 | imageView.snp.makeConstraints { make in 40 | make.edges.equalToSuperview() 41 | } 42 | })(playListCover) 43 | 44 | ({(effectView: UIVisualEffectView) in 45 | effectView.layer.masksToBounds = true 46 | effectView.layer.cornerRadius = 10 47 | effectView.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner] 48 | contentView.addSubview(effectView) 49 | effectView.snp.makeConstraints { make in 50 | make.left.right.bottom.equalToSuperview() 51 | make.height.equalTo(100) 52 | } 53 | })(effectView) 54 | 55 | ({(label: UILabel) in 56 | label.textColor = .white 57 | label.numberOfLines = 2 58 | label.textAlignment = .center 59 | label.font = .boldSystemFont(ofSize: 30) 60 | addSubview(label) 61 | label.snp.makeConstraints { make in 62 | make.centerY.equalTo(effectView) 63 | make.leading.equalTo(10) 64 | make.trailing.equalTo(-10) 65 | } 66 | })(titleLabel) 67 | } 68 | 69 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 70 | super.didUpdateFocus(in: context, with: coordinator) 71 | if isFocused { 72 | let scaleFactor = self.scaleFactor 73 | coordinator.addCoordinatedAnimations { 74 | self.transform = CGAffineTransformMakeScale(scaleFactor, scaleFactor) 75 | let scaleDiff = (self.bounds.size.height * scaleFactor - self.bounds.size.height) / 2 76 | self.transform = CGAffineTransformTranslate(self.transform, 0, -scaleDiff) 77 | self.layer.shadowOffset = CGSizeMake(0, 16) 78 | self.layer.shadowOpacity = 0.2 79 | self.layer.shadowRadius = 18.0 80 | self.layer.cornerRadius = 10 81 | self.addMotionEffect(self.motionEffectH) 82 | self.addMotionEffect(self.motionEffectV) 83 | } 84 | } else { 85 | coordinator.addCoordinatedAnimations { 86 | self.transform = CGAffineTransformIdentity 87 | self.layer.shadowOpacity = 0 88 | self.layer.shadowOffset = CGSizeMake(0, 0) 89 | self.layer.cornerRadius = 10 90 | self.removeMotionEffect(self.motionEffectH) 91 | self.removeMotionEffect(self.motionEffectV) 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /NeteaseTVDemo/WKPlayListTableViewCell.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import MarqueeLabel 4 | import NeteaseRequest 5 | class WKPlayListTableViewCell: UITableViewCell { 6 | 7 | private var picView = UIImageView () 8 | private var songNameLabel = MarqueeLabel() 9 | private var singerLabel = UILabel() 10 | private var timeLabel = UILabel() 11 | 12 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 13 | super.init(style: style, reuseIdentifier: reuseIdentifier) 14 | ({(picView: UIImageView) in 15 | picView.contentMode = .scaleAspectFill 16 | picView.layer.cornerRadius = 10 17 | picView.clipsToBounds = true 18 | contentView.addSubview(picView) 19 | picView.snp.makeConstraints { make in 20 | make.left.equalTo(contentView) 21 | make.top.bottom.equalTo(contentView) 22 | make.size.equalTo(100) 23 | } 24 | })(picView) 25 | 26 | ({(label: MarqueeLabel) in 27 | contentView.addSubview(label) 28 | label.snp.makeConstraints { make in 29 | make.left.equalTo(picView.snp.right).offset(20) 30 | make.right.equalTo(-140) 31 | make.bottom.equalTo(contentView.snp.centerY).offset(-5) 32 | } 33 | })(songNameLabel) 34 | 35 | ({(label: UILabel) in 36 | label.font = .systemFont(ofSize: 30) 37 | contentView.addSubview(label) 38 | label.snp.makeConstraints { make in 39 | make.left.equalTo(picView.snp.right).offset(20) 40 | make.right.equalTo(-140) 41 | make.top.equalTo(contentView.snp.centerY).offset(5) 42 | } 43 | })(singerLabel) 44 | 45 | ({(label: UILabel) in 46 | label.textColor = .lightGray 47 | label.font = .systemFont(ofSize: 30) 48 | contentView.addSubview(label) 49 | label.snp.makeConstraints { make in 50 | make.right.equalToSuperview().offset(-20) 51 | make.centerY.equalToSuperview() 52 | } 53 | })(timeLabel) 54 | 55 | } 56 | 57 | required init?(coder: NSCoder) { 58 | fatalError("init(coder:) has not been implemented") 59 | } 60 | 61 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 62 | super.didUpdateFocus(in: context, with: coordinator) 63 | if isFocused { 64 | songNameLabel.textColor = .black 65 | singerLabel.textColor = .black 66 | } else { 67 | songNameLabel.textColor = UIColor(dynamicProvider: { (traitCollection: UITraitCollection) -> UIColor in 68 | return traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark ? 69 | .white : .black 70 | }) 71 | singerLabel.textColor = UIColor(dynamicProvider: { (traitCollection: UITraitCollection) -> UIColor in 72 | return traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark ? 73 | .white : .black 74 | }) 75 | } 76 | } 77 | 78 | 79 | func setModel(_ model: CustomAudioModel) { 80 | self.picView.kf.setImage(with: URL(string: model.wk_audioPic ?? "")) 81 | self.songNameLabel.text = model.wk_sourceName 82 | self.singerLabel.text = model.wk_singerName 83 | self.timeLabel.text = model.audioTime 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /NeteaseTVDemo/WKSegmentCell.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | class WKSegmentCell: UICollectionViewCell { 5 | private var bgView = UIView() 6 | var selectedView = UIView() 7 | var titleLabel = UILabel() 8 | 9 | override init(frame: CGRect) { 10 | super.init(frame: frame) 11 | ({(view: UIView) in 12 | view.backgroundColor = UIColor.systemPink 13 | view.layer.shadowOffset = CGSizeMake(0, 10) 14 | view.layer.shadowOpacity = 0.15 15 | view.layer.shadowRadius = 16.0 16 | view.layer.cornerRadius = 30 17 | view.layer.cornerCurve = .continuous 18 | view.isHidden = true 19 | addSubview(view) 20 | view.snp.makeConstraints { make in 21 | make.edges.equalToSuperview() 22 | } 23 | })(selectedView) 24 | 25 | ({(view: UIView) in 26 | view.backgroundColor = UIColor(white: 0.9, alpha: 0.1) 27 | view.layer.shadowOffset = CGSizeMake(0, 10) 28 | view.layer.shadowOpacity = 0.15 29 | view.layer.shadowRadius = 16.0 30 | view.layer.cornerRadius = 30 31 | view.layer.cornerCurve = .continuous 32 | addSubview(view) 33 | view.snp.makeConstraints { make in 34 | make.edges.equalToSuperview() 35 | } 36 | })(bgView) 37 | 38 | ({(label: UILabel) in 39 | label.text = "歌手" 40 | label.textColor = .white 41 | label.textAlignment = .center 42 | contentView.addSubview(label) 43 | label.snp.makeConstraints { make in 44 | make.edges.equalToSuperview() 45 | } 46 | })(titleLabel) 47 | } 48 | 49 | required init?(coder: NSCoder) { 50 | fatalError("init(coder:) has not been implemented") 51 | } 52 | 53 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 54 | super.didUpdateFocus(in: context, with: coordinator) 55 | bgView.isHidden = !selectedView.isHidden 56 | bgView.backgroundColor = isFocused ? .white : UIColor(white: 0.9, alpha: 0.1) 57 | titleLabel.textColor = isFocused ? (selectedView.isHidden ? .black : .white) : .white 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /NeteaseTVDemo/WKSongTableViewCell.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import MarqueeLabel 4 | class WKSongTableViewCell: UITableViewCell { 5 | 6 | var indexLabel = UILabel() 7 | private var songNameLabel = MarqueeLabel() 8 | var albumLabel = MarqueeLabel() 9 | var timeLabel = UILabel() 10 | 11 | 12 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 13 | super.init(style: style, reuseIdentifier: reuseIdentifier) 14 | let selView = UIView() 15 | selView.backgroundColor = UIColor(white: 0.9, alpha: 0.1) 16 | self.selectedBackgroundView = selView 17 | 18 | ({(label: UILabel) in 19 | // label.textColor = .white 20 | label.font = .systemFont(ofSize: 30) 21 | self.contentView.addSubview(label) 22 | label.snp.makeConstraints { make in 23 | make.left.equalTo(self.contentView).offset(30) 24 | make.centerY.equalToSuperview() 25 | make.bottom.equalTo(self.contentView).offset(-40) 26 | } 27 | })(indexLabel) 28 | 29 | ({(label: MarqueeLabel) in 30 | self.contentView.addSubview(label) 31 | label.snp.makeConstraints { make in 32 | make.left.equalTo(self.contentView).offset(80) 33 | make.right.equalTo(self.contentView.snp.right).offset(-30) 34 | make.bottom.equalTo(self.contentView.snp.centerY).offset(-5) 35 | } 36 | })(songNameLabel) 37 | 38 | ({(label: MarqueeLabel) in 39 | label.font = .systemFont(ofSize: 30) 40 | self.contentView.addSubview(label) 41 | label.snp.makeConstraints { make in 42 | make.left.equalTo(self.contentView).offset(80) 43 | make.right.equalTo(self.contentView.snp.right).offset(-30) 44 | make.top.equalTo(self.contentView.snp.centerY).offset(5) 45 | } 46 | })(albumLabel) 47 | } 48 | 49 | required init?(coder: NSCoder) { 50 | fatalError("init(coder:) has not been implemented") 51 | } 52 | 53 | 54 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 55 | super.didUpdateFocus(in: context, with: coordinator) 56 | if isFocused { 57 | songNameLabel.textColor = .black 58 | albumLabel.textColor = .black 59 | indexLabel.textColor = .black 60 | } else { 61 | songNameLabel.textColor = UIColor(dynamicProvider: { (traitCollection: UITraitCollection) -> UIColor in 62 | return traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark ? 63 | .white : .black 64 | }) 65 | albumLabel.textColor = UIColor(dynamicProvider: { (traitCollection: UITraitCollection) -> UIColor in 66 | return traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark ? 67 | .white : .black 68 | }) 69 | indexLabel.textColor = UIColor(dynamicProvider: { (traitCollection: UITraitCollection) -> UIColor in 70 | return traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark ? 71 | .white : .black 72 | }) 73 | } 74 | } 75 | 76 | func setModel(_ model: CustomAudioModel) { 77 | if let tns = model.transTitle { 78 | self.songNameLabel.text = model.wk_sourceName! + " (\(tns))" 79 | let attributedString = NSMutableAttributedString(string: self.songNameLabel.text!) 80 | // 设置不同范围的文本颜色 81 | attributedString.addAttribute(.foregroundColor, value: UIColor.lightGray, range: NSRange(location: model.wk_sourceName!.count, length: tns.count + 3)) 82 | attributedString.addAttribute(.font, value: UIFont.systemFont(ofSize: 27), range: NSRange(location: model.wk_sourceName!.count, length: tns.count + 3)) 83 | self.songNameLabel.attributedText = attributedString 84 | } else { 85 | self.songNameLabel.text = model.wk_sourceName 86 | } 87 | self.albumLabel.text = model.albumTitle 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /NeteaseTVDemo/WKTabBarViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | class WKTabBarViewController: UITabBarController { 5 | 6 | static func creat() -> WKTabBarViewController { 7 | let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! WKTabBarViewController 8 | vc.modalPresentationStyle = .blurOverFullScreen 9 | return vc 10 | } 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | var vcs = [UIViewController]() 16 | 17 | let recommendVC = WKRecommendViewController.creat() 18 | recommendVC.tabBarItem.title = "推荐" 19 | vcs.append(recommendVC) 20 | 21 | let findVC = WKFindViewController.creat() 22 | findVC.tabBarItem.title = "浏览" 23 | vcs.append(findVC) 24 | 25 | let podcastVC = WKPodcastViewController.creat() 26 | podcastVC.tabBarItem.title = "播客" 27 | vcs.append(podcastVC) 28 | 29 | let mvVC = WKMVViewController.creat() 30 | mvVC.tabBarItem.title = "MV" 31 | vcs.append(mvVC) 32 | 33 | let playingVC = WKPlayingViewController.creat() 34 | playingVC.tabBarItem.title = "正在播放" 35 | vcs.append(playingVC) 36 | 37 | let profileVC = WKProfileViewController.creat() 38 | profileVC.tabBarItem.title = "我的" 39 | vcs.append(profileVC) 40 | 41 | let searchVC = WKSearchViewController() 42 | searchVC.tabBarItem.title = "搜索" 43 | vcs.append(searchVC) 44 | 45 | setViewControllers(vcs, animated: false) 46 | 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NeteaseTVDemo - tvOS 客户端(已改名Vibefy) 2 | 3 | 代码完全开源,支持以下功能: 4 | 5 | - 游客自动登录 6 | - 二维码登录 7 | - 多账号切换 8 | - TopShelf 9 | - 每日推荐(歌单、单曲) 10 | - 歌单、播客、MV 分类 11 | - 音视频播放器 12 | - 歌词滚动 13 | - 最近播放(单曲 声音 歌单 专辑 播客) 14 | - 我的收藏(歌单 专辑 播客) 15 | - 我的喜欢 16 | - 我的歌单 17 | - 我的音乐云盘 18 | - 搜索 19 | ------ 20 | 21 | ## 预览 22 | ![预览图](https://github.com/ZhangDo/NeteaseTVDemo/blob/main/images/preview.png) 23 | 24 | ## 安装 25 | - 自行编译安装 26 | - Vibefy: [TestFlight](https://testflight.apple.com/join/he8gBuuY) 27 | 28 | ## 提示 29 | 30 | 项目中的API服务是部署在我自己的腾讯云上的,但是为了自己的账号安全,请尽量使用自己部署的服务 31 | 32 | ## 支持平台 33 | Apple TV 34 | 35 | ## 项目动态 36 | ![Alt](https://repobeats.axiom.co/api/embed/71af082fd5501aa3498863b67b470bc2ec5496f2.svg "Repobeats analytics image") 37 | 38 | 39 | ## 开源代码: 40 | 41 | - Client: https://github.com/ZhangDo/NeteaseTVDemo 42 | - NeteaseAPI: https://github.com/ZhangDo/NeteaseRequest 43 | 44 | ## Telegram Group 45 | https://t.me/VibefyCircle 46 | 47 | ## QQ 交流群 48 | 49 | Vibefy QQ 交流群: 865021208 50 | 51 | ## 参考及引用 52 | 53 | [ATV-Bilibili-demo](https://github.com/yichengchen/ATV-Bilibili-demo) 54 | 55 | ## 声明 56 | 57 | 本项目的所有功能和资源都是基于公开的资料和技术开发的 58 | 59 | 本项目的目的是促进知识共享和技术交流,欢迎开发者和用户积极参与并为项目做出贡献。 60 | 61 | 本项目的所有代码和文档都是开放的 62 | 63 | -------------------------------------------------------------------------------- /TopShelf/ContentProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentProvider.swift 3 | // TopShelf 4 | // 5 | // Created by DLancerC on 2023/12/3. 6 | // 7 | 8 | import TVServices 9 | 10 | class ContentProvider: TVTopShelfContentProvider { 11 | 12 | override func loadTopShelfContent(completionHandler: @escaping (TVTopShelfContent?) -> Void) { 13 | var topShelfSections :[TVTopShelfItemCollection] = [] 14 | addCurrentPlayCollection(topshelfItems: &topShelfSections) 15 | addPlayListCollection(topshelfItems: &topShelfSections) 16 | let sectionedContent: TVTopShelfSectionedContent = TVTopShelfSectionedContent(sections: topShelfSections) 17 | completionHandler(sectionedContent) 18 | 19 | } 20 | 21 | func addCurrentPlayCollection(topshelfItems: inout [TVTopShelfItemCollection] ){ 22 | if let currentPlayDictionary = UserDefaults.standard.shareValue(forKey: "currentPlay"){ 23 | if let currentItem = creatContentItem(audioDictionary: currentPlayDictionary) { 24 | var currentItems = [TVTopShelfSectionedItem]() 25 | currentItems.append(currentItem) 26 | let currentItemCollection: TVTopShelfItemCollection = TVTopShelfItemCollection(items: currentItems) 27 | currentItemCollection.title = "当前播放" 28 | topshelfItems.append(currentItemCollection) 29 | } 30 | } 31 | } 32 | 33 | func addPlayListCollection(topshelfItems: inout [TVTopShelfItemCollection] ){ 34 | var playListItems = [TVTopShelfSectionedItem]() 35 | 36 | if let playList = UserDefaults.standard.shareListValue(forKey: "playList"){ 37 | playList.forEach{ (playAudioDictionary) in 38 | guard let contentItem = creatContentItem(audioDictionary: playAudioDictionary) else { return } 39 | playListItems.append(contentItem) 40 | } 41 | } 42 | 43 | let playListItemCollection: TVTopShelfItemCollection = TVTopShelfItemCollection(items: playListItems) 44 | playListItemCollection.title = "播放列表" 45 | topshelfItems.append(playListItemCollection) 46 | } 47 | 48 | func creatContentItem(audioDictionary: Dictionary) -> TVTopShelfSectionedItem? { 49 | guard let audioId = audioDictionary["audioId"] as? Int else { 50 | return nil 51 | } 52 | let contentItem = TVTopShelfSectionedItem(identifier: String(audioId)) 53 | contentItem.playAction = TVTopShelfAction(url: URL(string: "vibefy://" + String(audioId))!) 54 | contentItem.displayAction = TVTopShelfAction(url: URL(string: "vibefy://" + String(audioId))!) 55 | 56 | if let imageURL = audioDictionary["audioPicUrl"] as? String { 57 | contentItem.setImageURL(URL(string: imageURL), for: TVTopShelfItem.ImageTraits.screenScale1x) 58 | } 59 | 60 | if let audioTitle = audioDictionary["audioTitle"] as? String { 61 | contentItem.title = audioTitle 62 | } 63 | return contentItem 64 | } 65 | 66 | } 67 | 68 | extension UserDefaults { 69 | func shareListValue(forKey key: String) ->[Dictionary]? { 70 | guard let jsonString = UserDefaults.init(suiteName: "group.com.wk.Vibefy")?.value(forKey: key) as? String else { return nil } 71 | guard let jsonData = jsonString.data(using: .utf8) else { return nil } 72 | guard let listData = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [[String: Any]] else { return nil } 73 | return listData 74 | } 75 | 76 | func shareValue(forKey key: String) -> Dictionary? { 77 | guard let jsonString = UserDefaults.init(suiteName: "group.com.wk.Vibefy")?.value(forKey: key) as? String else { return nil } 78 | guard let jsonData = jsonString.data(using: .utf8) else { return nil } 79 | guard let dictionary = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { return nil } 80 | return dictionary 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /TopShelf/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.tv-top-shelf 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).ContentProvider 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /TopShelf/TopShelf.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.com.wk.Vibefy 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /images/WechatIMG95.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/images/WechatIMG95.png -------------------------------------------------------------------------------- /images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangDo/NeteaseTVDemo/bae3519263b07bf9abdd68e9c74084fa8cb5483b/images/preview.png --------------------------------------------------------------------------------