├── .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 | 
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 | 
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
--------------------------------------------------------------------------------