├── .gitignore
├── DKVideo.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── DKVideo.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── DKVideo
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon.png
│ │ ├── icon_20pt@2x.png
│ │ ├── icon_20pt@3x.png
│ │ ├── icon_29pt.png
│ │ ├── icon_29pt@2x.png
│ │ ├── icon_29pt@3x.png
│ │ ├── icon_40pt@2x.png
│ │ ├── icon_40pt@3x.png
│ │ ├── icon_60pt@2x.png
│ │ ├── icon_60pt@3x.png
│ │ ├── icon_76pt.png
│ │ ├── icon_76pt@2x.png
│ │ └── icon_83.5@2x.png
│ ├── Contents.json
│ ├── icon_cell_check.imageset
│ │ ├── Contents.json
│ │ └── icon_cell_check.pdf
│ ├── icon_cell_dir.imageset
│ │ ├── Contents.json
│ │ └── icon_cell_dir.pdf
│ ├── icon_cell_disclosure.imageset
│ │ ├── Contents.json
│ │ └── icon_cell_disclosure.pdf
│ ├── icon_cell_night_mode.imageset
│ │ ├── Contents.json
│ │ └── icon_cell_night_mode.pdf
│ ├── icon_cell_theme.imageset
│ │ ├── Contents.json
│ │ └── icon_cell_theme.pdf
│ ├── icon_common_select.imageset
│ │ ├── Contents.json
│ │ └── icon_common_select.png
│ ├── icon_day.imageset
│ │ ├── Contents.json
│ │ └── icon_day.png
│ ├── icon_navigation_back.imageset
│ │ ├── Contents.json
│ │ └── icon_navigation_back.pdf
│ ├── icon_navigation_close.imageset
│ │ ├── Contents.json
│ │ └── icon_navigation_close.pdf
│ ├── icon_navigation_forward.imageset
│ │ ├── Contents.json
│ │ └── icon_navigation_forward.pdf
│ ├── icon_navigation_refresh.imageset
│ │ ├── Contents.json
│ │ └── icon_navigation_refresh.pdf
│ ├── icon_navigation_stop.imageset
│ │ ├── Contents.json
│ │ └── icon_navigation_stop.pdf
│ ├── icon_navigation_theme.imageset
│ │ ├── Contents.json
│ │ └── icon_navigation_theme.pdf
│ ├── icon_navigation_web.imageset
│ │ ├── Contents.json
│ │ └── icon_navigation_web.pdf
│ ├── icon_night.imageset
│ │ ├── Contents.json
│ │ └── icon_night.png
│ ├── icon_tabbar_search.imageset
│ │ ├── Contents.json
│ │ └── icon_tabbar_search.pdf
│ ├── icon_tabbar_settings.imageset
│ │ ├── Contents.json
│ │ └── icon_tabbar_settings.pdf
│ ├── icon_vip.imageset
│ │ ├── Contents.json
│ │ └── icon_vip.png
│ ├── icon_whatsnew_theme.imageset
│ │ ├── Contents.json
│ │ └── icon_whatsnew_theme.pdf
│ ├── icon_whatsnew_whats_new.imageset
│ │ ├── Contents.json
│ │ └── icon_whatsnew_whats_new.pdf
│ └── pic_common_404.imageset
│ │ ├── Contents.json
│ │ ├── pic_common_404@2x.png
│ │ └── pic_common_404@3x.png
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── Extensions
│ ├── Fundation
│ │ ├── HandyJson+SwiftyJson.swift
│ │ ├── Notification+Custom.swift
│ │ ├── RuntimeExtension.swift
│ │ ├── String+Utils.swift
│ │ └── UserDefaults+Custom.swift
│ ├── RxSwift
│ │ ├── KafkaRefresh+Rx.swift
│ │ └── Observable+Operators.swift
│ └── UIKit
│ │ ├── UIView+Utils.swift
│ │ └── UIViewController+Utils.swift
├── Info.plist
├── LaunchViewController.swift
├── Managers
│ ├── ActivityIndicator.swift
│ ├── HUDUtils.swift
│ ├── LibsManager.swift
│ ├── LogManager.swift
│ ├── ThemeManager.swift
│ └── URLIntercept.swift
├── Modules
│ ├── Common
│ │ ├── BaseTableViewCell.swift
│ │ ├── CollectionFlowLayout.swift
│ │ ├── CommonSelectVC.swift
│ │ ├── NavigationController.swift
│ │ ├── PageViewController.swift
│ │ ├── PageViewController2.swift
│ │ ├── PopUpMenu.swift
│ │ ├── TableCellViewModel.swift
│ │ ├── TableViewCell.swift
│ │ ├── TableViewController.swift
│ │ ├── TestWebVC.swift
│ │ ├── TextView.swift
│ │ ├── VideoPlayerVC.swift
│ │ ├── View.swift
│ │ ├── ViewController.swift
│ │ ├── ViewModelType.swift
│ │ ├── VipWebsites.swift
│ │ └── WebViewController.swift
│ ├── Download
│ │ ├── DKM3u8Helper.swift
│ │ ├── DownLoadManage.swift
│ │ ├── DownloadViewController.swift
│ │ ├── M3U8Downloader.swift
│ │ ├── SegmentDownloader.swift
│ │ └── VideoDownloader.swift
│ ├── Home
│ │ ├── HomeTabbarVC.swift
│ │ └── HomeViewController.swift
│ └── Settings
│ │ ├── SettingViewCells.swift
│ │ ├── SettingViewController.swift
│ │ └── SettingViewModel.swift
└── Resources
│ ├── AdStringFile.swift
│ ├── Platform.json
│ ├── R.generated.swift
│ ├── Vipwebsites.json
│ ├── blank.caf
│ └── index.html
├── HowToInterceptRequests.md
├── LICENSE
├── Podfile
├── Podfile.lock
├── Readme.md
└── VideoShare
├── Info.plist
├── ShareVC.swift
├── ShareViewController.swift
└── VideoShare.entitlements
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 | Pods
92 | IPADir
93 | exportTest.plist
94 | archive.sh
95 | exportAppstore.plist
96 |
--------------------------------------------------------------------------------
/DKVideo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/DKVideo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/DKVideo.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/DKVideo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/DKVideo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/3.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Aspects
10 | import RxSwift
11 | import SuperPlayer
12 | import Tiercel
13 | import UIKit
14 | import WebKit
15 | import AVFoundation
16 | let appDelegate = UIApplication.shared.delegate as! AppDelegate
17 |
18 | @UIApplicationMain
19 | class AppDelegate: UIResponder, UIApplicationDelegate, URLSessionDelegate {
20 | var window: UIWindow?
21 | /* 无声音频播放器 */
22 | var blankPlayer: AVAudioPlayer?
23 | /* 后台任务标识符 */
24 | var bgTaskIdentifier: UIBackgroundTaskIdentifier!
25 | var bgTaskTimer: Timer?
26 |
27 | var sessionManagerBackground: SessionManager = {
28 | var configuration = SessionConfiguration()
29 | configuration.allowsCellularAccess = UserDefaults.downloadWithoutWifi
30 | let manager = SessionManager("com.dkjone.DKVideo.normal", configuration: configuration, operationQueue: DispatchQueue(label: "com.dkjone.DKVideo.Normal"))
31 | return manager
32 | }()
33 |
34 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
35 | let browCont = WKWebView().value(forKey: "browsingContextController")
36 | let classType = type(of: browCont!) as! AnyClass
37 | if let method = extractMethodFrom(owner: classType, selector: NSSelectorFromString("registerSchemeForCustomProtocol:")) {
38 | _ = method("http")
39 | _ = method("https")
40 | }
41 |
42 | URLProtocol.registerClass(URLIntercept.self)
43 | LibsManager.shared.configTheme()
44 | URLIntercept.videoUrl.filterEmpty().distinctUntilChanged {
45 | $1.contains("127.0.0.1") || $0 == $1
46 | }.throttle(.seconds(2), scheduler: MainScheduler.asyncInstance).observeOn(MainScheduler.asyncInstance).bind { urlStr in
47 | let videoVC = VideoPlayerVC.shared
48 | if videoVC.isVisible {
49 | let playerModel = videoVC.playerView.playerModel
50 | let playurl = SuperPlayerUrl()
51 | playurl.title = Date().string(withFormat: "yyyyMMddHHmmss1")
52 | playurl.url = urlStr
53 | playerModel?.multiVideoURLs.append(playurl)
54 | videoVC.playerView.play(with: playerModel!)
55 | print("----------\(playurl)------")
56 | } else {
57 | videoVC.urlStr = urlStr
58 | VideoPlayerVC.show()
59 | }
60 | }.disposed(by: rx.disposeBag)
61 |
62 | return true
63 | }
64 |
65 | func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
66 | if !url.absoluteString.contains("DKVideo://") { return false }
67 | let videoUrl = url.absoluteString.removingPrefix("DKVideo://")
68 | print("openUrl:\(videoUrl)")
69 | let webvc = WebViewController()
70 | webvc.requestURL = URL(string: videoUrl)
71 | if let navc = UINavigationController.currentViewController()?.navigationController {
72 | navc.pushViewController(webvc)
73 | } else {
74 | waitToPresentVC = nil
75 | }
76 | return true
77 | }
78 |
79 | func applicationWillEnterForeground(_ application: UIApplication) {}
80 |
81 | func applicationDidEnterBackground(_ application: UIApplication) {
82 | if UserDefaults.backgroundDownload{ enterBackgroundHandler()}
83 | }
84 |
85 | func applicationDidBecomeActive(_ application: UIApplication) {}
86 |
87 | // 程序进入后台处理
88 | func enterBackgroundHandler() {
89 | let app = UIApplication.shared
90 | bgTaskIdentifier = app.beginBackgroundTask(expirationHandler: {
91 | app.endBackgroundTask(self.bgTaskIdentifier)
92 | self.bgTaskIdentifier = UIBackgroundTaskIdentifier.invalid
93 | })
94 | bgTaskTimer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(requestMoreTime), userInfo: nil, repeats: true)
95 | bgTaskTimer?.fire()
96 | }
97 |
98 | @objc func requestMoreTime() {
99 | if UIApplication.shared.backgroundTimeRemaining < 30 {
100 | playBlankAudio()
101 | UIApplication.shared.endBackgroundTask(bgTaskIdentifier)
102 | bgTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
103 | UIApplication.shared.endBackgroundTask(self.bgTaskIdentifier)
104 | self.bgTaskIdentifier = UIBackgroundTaskIdentifier.invalid
105 | })
106 | }
107 | }
108 |
109 | // 播放无声音频
110 | func playBlankAudio() {
111 | playAudio(forResource: "blank", ofType: "caf")
112 | }
113 |
114 | // 开始播放音频
115 | func playAudio(forResource resource: String?, ofType: String?) {
116 | try? AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
117 | try? AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation) // .setActive(true, error: &activationErr)
118 | let blankSoundURL = R.file.blankCaf() // URL(string: Bundle.main.path(forResource: resource, ofType: ofType) ?? "")
119 |
120 | if let blankSoundURL = blankSoundURL {
121 | blankPlayer = try? AVAudioPlayer(contentsOf: blankSoundURL)
122 | blankPlayer?.play()
123 | }
124 | }
125 | }
126 |
127 | extension AppDelegate {
128 | func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
129 | let downloadManagers = [ sessionManagerBackground]
130 | for manager in downloadManagers {
131 | if manager.identifier == identifier {
132 | manager.completionHandler = completionHandler
133 | break
134 | }
135 | }
136 | }
137 | }
138 |
139 | var waitToPresentVC: UIViewController?
140 |
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "icon_20pt@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "icon_20pt@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "icon_29pt.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "icon_29pt@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "icon_29pt@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "icon_40pt@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "icon_40pt@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "icon_60pt@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "icon_60pt@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "idiom" : "ipad",
59 | "size" : "20x20",
60 | "scale" : "1x"
61 | },
62 | {
63 | "idiom" : "ipad",
64 | "size" : "20x20",
65 | "scale" : "2x"
66 | },
67 | {
68 | "idiom" : "ipad",
69 | "size" : "29x29",
70 | "scale" : "1x"
71 | },
72 | {
73 | "idiom" : "ipad",
74 | "size" : "29x29",
75 | "scale" : "2x"
76 | },
77 | {
78 | "idiom" : "ipad",
79 | "size" : "40x40",
80 | "scale" : "1x"
81 | },
82 | {
83 | "idiom" : "ipad",
84 | "size" : "40x40",
85 | "scale" : "2x"
86 | },
87 | {
88 | "size" : "76x76",
89 | "idiom" : "ipad",
90 | "filename" : "icon_76pt.png",
91 | "scale" : "1x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "icon_76pt@2x.png",
97 | "scale" : "2x"
98 | },
99 | {
100 | "size" : "83.5x83.5",
101 | "idiom" : "ipad",
102 | "filename" : "icon_83.5@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "1024x1024",
107 | "idiom" : "ios-marketing",
108 | "filename" : "Icon.png",
109 | "scale" : "1x"
110 | }
111 | ],
112 | "info" : {
113 | "version" : 1,
114 | "author" : "xcode"
115 | }
116 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/Icon.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_29pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_29pt.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_76pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_76pt.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_cell_check.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_cell_check.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_cell_check.imageset/icon_cell_check.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_cell_check.imageset/icon_cell_check.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_cell_dir.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_cell_dir.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_cell_dir.imageset/icon_cell_dir.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_cell_dir.imageset/icon_cell_dir.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_cell_disclosure.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_cell_disclosure.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_cell_disclosure.imageset/icon_cell_disclosure.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_cell_disclosure.imageset/icon_cell_disclosure.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_cell_night_mode.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_cell_night_mode.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_cell_night_mode.imageset/icon_cell_night_mode.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_cell_night_mode.imageset/icon_cell_night_mode.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_cell_theme.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_cell_theme.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_cell_theme.imageset/icon_cell_theme.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_cell_theme.imageset/icon_cell_theme.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_common_select.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "icon_common_select.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_common_select.imageset/icon_common_select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_common_select.imageset/icon_common_select.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_day.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "idiom" : "universal",
13 | "filename" : "icon_day.png",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_day.imageset/icon_day.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_day.imageset/icon_day.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_back.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_navigation_back.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_back.imageset/icon_navigation_back.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_back.imageset/icon_navigation_back.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_close.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_navigation_close.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_close.imageset/icon_navigation_close.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_close.imageset/icon_navigation_close.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_forward.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_navigation_forward.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_forward.imageset/icon_navigation_forward.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_forward.imageset/icon_navigation_forward.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_refresh.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_navigation_refresh.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_refresh.imageset/icon_navigation_refresh.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_refresh.imageset/icon_navigation_refresh.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_stop.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_navigation_stop.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_stop.imageset/icon_navigation_stop.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_stop.imageset/icon_navigation_stop.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_theme.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_navigation_theme.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_theme.imageset/icon_navigation_theme.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_theme.imageset/icon_navigation_theme.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_web.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_navigation_web.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_navigation_web.imageset/icon_navigation_web.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_navigation_web.imageset/icon_navigation_web.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_night.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "idiom" : "universal",
13 | "filename" : "icon_night.png",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_night.imageset/icon_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_night.imageset/icon_night.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_tabbar_search.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_tabbar_search.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_tabbar_search.imageset/icon_tabbar_search.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_tabbar_search.imageset/icon_tabbar_search.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_tabbar_settings.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_tabbar_settings.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_tabbar_settings.imageset/icon_tabbar_settings.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_tabbar_settings.imageset/icon_tabbar_settings.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_vip.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "idiom" : "universal",
13 | "filename" : "icon_vip.png",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_vip.imageset/icon_vip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_vip.imageset/icon_vip.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_whatsnew_theme.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_whatsnew_theme.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_whatsnew_theme.imageset/icon_whatsnew_theme.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_whatsnew_theme.imageset/icon_whatsnew_theme.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_whatsnew_whats_new.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon_whatsnew_whats_new.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/icon_whatsnew_whats_new.imageset/icon_whatsnew_whats_new.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/icon_whatsnew_whats_new.imageset/icon_whatsnew_whats_new.pdf
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/pic_common_404.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "pic_common_404@2x.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "pic_common_404@3x.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/pic_common_404.imageset/pic_common_404@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/pic_common_404.imageset/pic_common_404@2x.png
--------------------------------------------------------------------------------
/DKVideo/Assets.xcassets/pic_common_404.imageset/pic_common_404@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Assets.xcassets/pic_common_404.imageset/pic_common_404@3x.png
--------------------------------------------------------------------------------
/DKVideo/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 |
--------------------------------------------------------------------------------
/DKVideo/Base.lproj/Main.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 |
--------------------------------------------------------------------------------
/DKVideo/Extensions/Fundation/HandyJson+SwiftyJson.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HandyJson+SwiftyJson.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/9.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | import HandyJSON
12 | import SwiftyJSON
13 |
14 | protocol JsonInit {
15 | static func from(json: JSON) -> Self
16 | }
17 |
18 | extension HandyJSON {
19 | static func from(json: JSON) -> Self {
20 | return Self.deserialize(from: (json.rawString() ?? "")) ?? Self()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/DKVideo/Extensions/Fundation/Notification+Custom.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Notification+Custom.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/5/31.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension Notification.Name {
12 | /// 接收推送消息
13 | struct Message {
14 | /// 预警消息
15 | static let warning = Notification.Name(rawValue: "sjzx.notifications.earlyWarning")
16 |
17 | /// 通知消息
18 | static let msg = Notification.Name(rawValue: "sjzx.notifications.msg")
19 | }
20 | static let ProjectSeted = Notification.Name(rawValue: "sjzx.notifications.project")
21 | static let UserInfoChanged = Notification.Name(rawValue: "sjzx.notifications.UserInfoChanged")
22 | }
23 |
--------------------------------------------------------------------------------
/DKVideo/Extensions/Fundation/RuntimeExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RuntimeExtension.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/4.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /*: Example
12 | if let method = extractMethodFrom(owner: classOrObject, selector: NSSelectorFromString("methodName")) {
13 | method("Object")
14 | }
15 | */
16 |
17 |
18 | /// 动态获取方法
19 | /// - Parameters:
20 | /// - owner: 对象或者类对象
21 | /// - selector: 方法选择子
22 | func extractMethodFrom(owner: AnyObject, selector: Selector) -> ((Any?) -> Any)? {
23 | let method: Method?
24 | if owner is AnyClass {
25 | method = class_getClassMethod(owner as? AnyClass, selector)
26 | } else {
27 | method = class_getInstanceMethod(type(of: owner), selector)
28 | }
29 |
30 | if let one = method {
31 | let implementation = method_getImplementation(one)
32 |
33 | typealias Function = @convention(c) (AnyObject, Selector, Any?) -> Void
34 |
35 | let function = unsafeBitCast(implementation, to: Function.self)
36 |
37 | return { userinfo in function(owner, selector, userinfo) }
38 |
39 | } else {
40 | return nil
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/DKVideo/Extensions/Fundation/String+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Utils.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/4/1.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CommonCrypto
11 |
12 | extension String{
13 |
14 | var dk_dateAndTime:String{
15 | return self.slicing(from: 0, length: 16) ?? ""
16 | }
17 | var dk_date:String{
18 | return self.slicing(from: 0, length: 10) ?? ""
19 | }
20 |
21 | var md5: String {
22 | guard let data = self.data(using: .utf8) else {
23 | return self
24 | }
25 | var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
26 | #if swift(>=5.0)
27 | _ = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
28 | return CC_MD5(bytes.baseAddress, CC_LONG(data.count), &digest)
29 | }
30 | #else
31 | _ = data.withUnsafeBytes { bytes in
32 | return CC_MD5(bytes, CC_LONG(data.count), &digest)
33 | }
34 | #endif
35 |
36 | return digest.map { String(format: "%02x", $0) }.joined()
37 | }
38 |
39 | /// 判断是不是九宫格
40 | /// - Returns: true false
41 | func isNineKeyBoard()->Bool{
42 | let other : NSString = "➋➌➍➎➏➐➑➒"
43 | let len = self.count
44 | for _ in 0 ..< len {
45 | if !(other.range(of: self).location != NSNotFound) {
46 | return false
47 | }
48 | }
49 | return true
50 | }
51 | /// 判断是不是Emoji
52 | /// - Returns: true false
53 | func containsEmoji()->Bool{
54 | for scalar in unicodeScalars {
55 | switch scalar.value {
56 | case 0x1F600...0x1F64F,
57 | 0x1F300...0x1F5FF,
58 | 0x1F680...0x1F6FF,
59 | 0x2600...0x26FF,
60 | 0x2700...0x27BF,
61 | 0xFE00...0xFE0F:
62 | return true
63 | default:
64 | continue
65 | }
66 | }
67 | return false
68 | }
69 |
70 | /// 判断是不是Emoji
71 | /// - Returns: true false
72 | func hasEmoji()->Bool {
73 | let pattern = "[^\\u0020-\\u007E\\u00A0-\\u00BE\\u2E80-\\uA4CF\\uF900-\\uFAFF\\uFE30-\\uFE4F\\uFF00-\\uFFEF\\u0080-\\u009F\\u2000-\\u201f\r\n]"
74 | let pred = NSPredicate(format: "SELF MATCHES %@",pattern)
75 | return pred.evaluate(with: self)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/DKVideo/Extensions/Fundation/UserDefaults+Custom.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaults+Custom.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/3/7.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - 自定义缓存
12 |
13 | extension UserDefaults {
14 | /// 是否开启黑色模式
15 | var isDark: Bool {
16 | get { return bool(forKey: #function) }
17 | set { setValue(newValue, forKey: #function) }
18 | }
19 |
20 | var themeColor: Int {
21 | get { return integer(forKey: #function) }
22 | set { setValue(newValue, forKey: #function) }
23 | }
24 |
25 | /// 只检查一次黑暗模式
26 | static var hasCheckedDarkModel: Bool {
27 | get { standard.bool(forKey: #function) }
28 | set { standard.setValue(newValue, forKey: #function) }
29 | }
30 |
31 | /// 上一次打开的应用版本
32 | static var lastVersion: String {
33 | get { standard.string(forKey: #function) ?? "" }
34 | set { standard.setValue(newValue, forKey: #function) }
35 | }
36 |
37 | static var currentVip: VipAnalysis {
38 | get {
39 | return .from(json: JSON(standard.string(forKey: #function) ?? ""))
40 | }
41 | set {
42 | standard.setValue(newValue.toJSONString() ?? "", forKey: #function)
43 | }
44 | }
45 |
46 | /// 模仿电脑版请求
47 | static var isPCAgent: Bool {
48 | get { return standard.bool(forKey: #function) }
49 | set { standard.setValue(newValue, forKey: #function) }
50 | }
51 |
52 | /// 打开APP自动开始下载
53 | static var autoStartDownload: Bool {
54 | get { return standard.bool(forKey: #function) }
55 | set { standard.setValue(newValue, forKey: #function) }
56 | }
57 | /// APP后台时下载
58 | static var backgroundDownload: Bool {
59 | get { return standard.bool(forKey: #function) }
60 | set { standard.setValue(newValue, forKey: #function) }
61 | }
62 |
63 | /// APP使用流量时下载
64 | static var downloadWithoutWifi: Bool {
65 | get { return standard.bool(forKey: #function) }
66 | set { standard.setValue(newValue, forKey: #function) }
67 | }
68 |
69 | /// 使用WKWebview
70 | static var useWKWebview: Bool {
71 | get { return standard.bool(forKey: #function) }
72 | set { standard.setValue(newValue, forKey: #function) }
73 | }
74 |
75 | // 显示解析的Webview
76 | static var showVipWebView: Bool {
77 | get { return standard.bool(forKey: #function) }
78 | set { standard.setValue(newValue, forKey: #function) }
79 | }
80 |
81 | static var defaultConfig: Bool {
82 | get { return standard.bool(forKey: #function) }
83 | set { standard.setValue(newValue, forKey: #function) }
84 | }
85 |
86 | /// 同一个任务最多的Ts下载个数
87 | static var maxDownloadTS: Int {
88 | get { max(standard.integer(forKey: #function), 3)}
89 | set { standard.setValue(newValue, forKey: #function) }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/DKVideo/Extensions/RxSwift/KafkaRefresh+Rx.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KafkaRefresh+Rx.swift
3 | //
4 | //
5 | // Created by 朱德坤 on 2019/3/7.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxCocoa
11 | import RxSwift
12 | import KafkaRefresh
13 |
14 | extension Reactive where Base: KafkaRefreshControl {
15 |
16 | public var isAnimating: Binder {
17 | return Binder(self.base) { refreshControl, active in
18 | if active {
19 | refreshControl.beginRefreshing()
20 | } else {
21 | refreshControl.endRefreshing()
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/DKVideo/Extensions/RxSwift/Observable+Operators.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Observable+Operators.swift
3 | // szwhExpressway
4 | //
5 | // Created by 朱德坤 on 2019/3/6.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 | import RxCocoa
12 | import RxGesture
13 |
14 | extension Reactive where Base: UIView {
15 | func tap() -> Observable {
16 | return tapGesture().when(.recognized).mapToVoid()
17 | }
18 | }
19 | extension Reactive where Base: UIImageView {
20 | public var isHighlighted: Binder {
21 | return Binder(self.base) { view, attr in
22 | view.isHighlighted = attr
23 | }
24 | }
25 | }
26 |
27 | extension Reactive where Base:UITextField{
28 | public var attributedPlaceholder: Binder {
29 | return Binder(self.base){ filed,attr in
30 | filed.attributedPlaceholder = attr
31 | }
32 | }
33 | }
34 |
35 | protocol OptionalType {
36 | associatedtype Wrapped
37 |
38 | var value: Wrapped? { get }
39 | }
40 |
41 | extension Optional: OptionalType {
42 | var value: Wrapped? {
43 | return self
44 | }
45 | }
46 |
47 | extension Observable where Element: OptionalType {
48 | func filterNil() -> Observable {
49 | return flatMap { (element) -> Observable in
50 | if let value = element.value {
51 | return .just(value)
52 | } else {
53 | return .empty()
54 | }
55 | }
56 | }
57 |
58 | func filterNilKeepOptional() -> Observable {
59 | return self.filter { (element) -> Bool in
60 | return element.value != nil
61 | }
62 | }
63 |
64 | func replaceNil(with nilValue: Element.Wrapped) -> Observable {
65 | return flatMap { (element) -> Observable in
66 | if let value = element.value {
67 | return .just(value)
68 | } else {
69 | return .just(nilValue)
70 | }
71 | }
72 | }
73 | }
74 |
75 | protocol BooleanType {
76 | var boolValue: Bool { get }
77 | }
78 | extension Bool: BooleanType {
79 | var boolValue: Bool { return self }
80 | }
81 |
82 | // Maps true to false and vice versa
83 | extension Observable where Element: BooleanType {
84 | func not() -> Observable {
85 | return self.map { input in
86 | return !input.boolValue
87 | }
88 | }
89 | }
90 |
91 | extension Observable where Element: Equatable {
92 | func ignore(value: Element) -> Observable {
93 | return filter { (selfE) -> Bool in
94 | return value != selfE
95 | }
96 | }
97 | }
98 |
99 | extension ObservableType where E == Bool {
100 | /// Boolean not operator
101 | public func not() -> Observable {
102 | return self.map(!)
103 | }
104 | }
105 |
106 | extension SharedSequenceConvertibleType {
107 | func mapToVoid() -> SharedSequence {
108 | return map { _ in }
109 | }
110 | }
111 |
112 | extension ObservableType {
113 |
114 | func catchErrorJustComplete() -> Observable {
115 | return catchError { _ in
116 | return Observable.empty()
117 | }
118 | }
119 |
120 | func asDriverOnErrorJustComplete() -> Driver {
121 | return asDriver { error in
122 | assertionFailure("Error \(error)")
123 | return Driver.empty()
124 | }
125 | }
126 |
127 | func mapToVoid() -> Observable {
128 | return map { _ in }
129 | }
130 | }
131 |
132 | //https://gist.github.com/brocoo/aaabf12c6c2b13d292f43c971ab91dfa
133 | extension Reactive where Base: UIScrollView {
134 | public var reachedBottom: Observable {
135 | let scrollView = self.base as UIScrollView
136 | return self.contentOffset.flatMap { [weak scrollView] (contentOffset) -> Observable in
137 | guard let scrollView = scrollView else { return Observable.empty() }
138 | let visibleHeight = scrollView.frame.height - self.base.contentInset.top - scrollView.contentInset.bottom
139 | let y = contentOffset.y + scrollView.contentInset.top
140 | let threshold = max(0.0, scrollView.contentSize.height - visibleHeight)
141 | return (y > threshold) ? Observable.just(()) : Observable.empty()
142 | }
143 | }
144 | }
145 |
146 | // Two way binding operator between control property and variable, that's all it takes {
147 |
148 | infix operator <-> : DefaultPrecedence
149 |
150 | func nonMarkedText(_ textInput: UITextInput) -> String? {
151 | let start = textInput.beginningOfDocument
152 | let end = textInput.endOfDocument
153 |
154 | guard let rangeAll = textInput.textRange(from: start, to: end),
155 | let text = textInput.text(in: rangeAll) else {
156 | return nil
157 | }
158 |
159 | guard let markedTextRange = textInput.markedTextRange else {
160 | return text
161 | }
162 |
163 | guard let startRange = textInput.textRange(from: start, to: markedTextRange.start),
164 | let endRange = textInput.textRange(from: markedTextRange.end, to: end) else {
165 | return text
166 | }
167 |
168 | return (textInput.text(in: startRange) ?? "") + (textInput.text(in: endRange) ?? "")
169 | }
170 |
171 | func <-> (textInput: TextInput, variable: BehaviorRelay) -> Disposable {
172 | let bindToUIDisposable = variable.asObservable()
173 | .bind(to: textInput.text)
174 | let bindToVariable = textInput.text
175 | .subscribe(onNext: { [weak base = textInput.base] value in
176 | guard let base = base else {
177 | return
178 | }
179 |
180 | let nonMarkedTextValue = nonMarkedText(base)
181 |
182 | /**
183 | In some cases `textInput.textRangeFromPosition(start, toPosition: end)` will return nil even though the underlying
184 | value is not nil. This appears to be an Apple bug. If it's not, and we are doing something wrong, please let us know.
185 | The can be reproed easily if replace bottom code with
186 |
187 | if nonMarkedTextValue != variable.value {
188 | variable.value = nonMarkedTextValue ?? ""
189 | }
190 |
191 | and you hit "Done" button on keyboard.
192 | */
193 | if let nonMarkedTextValue = nonMarkedTextValue, nonMarkedTextValue != variable.value {
194 | variable.accept(nonMarkedTextValue)
195 | }
196 | }, onCompleted: {
197 | bindToUIDisposable.dispose()
198 | })
199 |
200 | return Disposables.create(bindToUIDisposable, bindToVariable)
201 | }
202 |
203 | func <-> (property: ControlProperty, variable: BehaviorRelay) -> Disposable {
204 | if T.self == String.self {
205 | #if DEBUG
206 | fatalError("It is ok to delete this message, but this is here to warn that you are maybe trying to bind to some `rx.text` property directly to variable.\n" +
207 | "That will usually work ok, but for some languages that use IME, that simplistic method could cause unexpected issues because it will return intermediate results while text is being inputed.\n" +
208 | "REMEDY: Just use `textField <-> variable` instead of `textField.rx.text <-> variable`.\n" +
209 | "Find out more here: https://github.com/ReactiveX/RxSwift/issues/649\n"
210 | )
211 | #endif
212 | }
213 |
214 | let bindToUIDisposable = variable.asObservable()
215 | .bind(to: property)
216 | let bindToVariable = property
217 | .subscribe(onNext: { value in
218 | variable.accept(value)
219 | }, onCompleted: {
220 | bindToUIDisposable.dispose()
221 | })
222 |
223 | return Disposables.create(bindToUIDisposable, bindToVariable)
224 | }
225 |
--------------------------------------------------------------------------------
/DKVideo/Extensions/UIKit/UIViewController+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+Utils.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/3/28.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | let safeAreaBottomHeight = CGFloat(UIApplication.shared.statusBarFrame.height == 44 ? 34 : 0)
12 | let safeAreaTopHeight = UIApplication.shared.statusBarFrame.height
13 | let screenWidth = UIScreen.main.bounds.width
14 | let screenHeight = UIScreen.main.bounds.height
15 |
16 | extension UIViewController {
17 | class func currentViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
18 | if let nav = base as? UINavigationController {
19 | return currentViewController(base: nav.visibleViewController)
20 | }
21 | if let tab = base as? UITabBarController {
22 | return currentViewController(base: tab.selectedViewController)
23 | }
24 | if let presented = base?.presentedViewController {
25 | return currentViewController(base: presented)
26 | }
27 | if let main = (base as? UISplitViewController)?.children.last {
28 | return currentViewController(base: main)
29 | }
30 | return base
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/DKVideo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleURLTypes
20 |
21 |
22 | CFBundleTypeRole
23 | Editor
24 | CFBundleURLName
25 | DKVideo
26 | CFBundleURLSchemes
27 |
28 | DKVideo
29 |
30 |
31 |
32 | CFBundleVersion
33 | 1
34 | LSRequiresIPhoneOS
35 |
36 | NSAppTransportSecurity
37 |
38 | NSAllowsArbitraryLoads
39 |
40 | NSExceptionDomains
41 |
42 | jpush.cn
43 |
44 | NSExceptionAllowsInsecureHTTPLoads
45 |
46 | NSIncludesSubdomains
47 |
48 |
49 |
50 |
51 | UIBackgroundModes
52 |
53 | audio
54 | fetch
55 | processing
56 |
57 | UILaunchStoryboardName
58 | LaunchScreen
59 | UIMainStoryboardFile
60 | Main
61 | UIRequiredDeviceCapabilities
62 |
63 | armv7
64 |
65 | UISupportedInterfaceOrientations
66 |
67 | UIInterfaceOrientationPortraitUpsideDown
68 | UIInterfaceOrientationPortrait
69 |
70 | UISupportedInterfaceOrientations~ipad
71 |
72 | UIInterfaceOrientationPortrait
73 | UIInterfaceOrientationPortraitUpsideDown
74 | UIInterfaceOrientationLandscapeLeft
75 | UIInterfaceOrientationLandscapeRight
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/DKVideo/LaunchViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LaunchViewController.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/6.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 | import WebKit
11 | class LaunchViewController: ViewController {
12 | override func makeUI() {
13 | view.backgroundColor = .black
14 | let webview = WKWebView()
15 | webview.backgroundColor = .black
16 | view.addSubview(webview)
17 | webview.snp.makeConstraints {
18 | $0.edges.equalTo(UIEdgeInsets(top: -50, left: 0, bottom: -50, right: 0))
19 | }
20 | if let url = Bundle.main.url(forResource: "index", withExtension: "html") {
21 | var htmlStr = (try? String(contentsOf: url)) ?? ""
22 | htmlStr = htmlStr.replacingOccurrences(of: "screenWidth", with: "\(screenWidth)")
23 | htmlStr = htmlStr.replacingOccurrences(of: "screenHeight", with: "\(screenHeight)")
24 | // webview.loadHTMLString(htmlStr, baseURL: nil)
25 | }
26 |
27 | DispatchQueue.main.asyncAfter(deadline: .now() + 7) { [weak self] in
28 | self?.switchToHome()
29 | }
30 | }
31 |
32 | override func bindViewModel() {
33 | view.rx.tap().delay(.seconds(1), scheduler: MainScheduler.asyncInstance).bind { [unowned self] _ in
34 | //self.switchToHome()
35 | }.disposed(by: rx.disposeBag)
36 | }
37 | override func viewDidAppear(_ animated: Bool) {
38 | super.viewDidAppear(animated)
39 | switchToHome()
40 | }
41 |
42 | func switchToHome() {
43 | let vc = UISplitViewController()
44 | vc.maximumPrimaryColumnWidth = screenWidth / 2
45 | vc.preferredPrimaryColumnWidthFraction = 0.5
46 | vc.viewControllers = [HomeTabbarVC(), NavigationController(rootViewController: WebViewController())]
47 | vc.preferredDisplayMode = .primaryOverlay
48 | keyWindow.rootViewController = vc
49 | }
50 | }
51 |
52 | var currentWebVC: WebViewController {
53 | if let vc = ((keyWindow.rootViewController as? UISplitViewController)?.viewControllers.last?.children.first as? WebViewController) {
54 | return vc
55 | }
56 | return WebViewController()
57 | }
58 |
--------------------------------------------------------------------------------
/DKVideo/Managers/ActivityIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivityIndicator.swift
3 | // DKVideo
4 | //
5 | // Created by Krunoslav Zaher on 10/18/15.
6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved.
7 | //
8 |
9 | #if !RX_NO_MODULE
10 | import RxSwift
11 | import RxCocoa
12 | #endif
13 |
14 | private struct ActivityToken : ObservableConvertibleType, Disposable {
15 | private let _source: Observable
16 | private let _dispose: Cancelable
17 |
18 | init(source: Observable, disposeAction: @escaping () -> Void) {
19 | _source = source
20 | _dispose = Disposables.create(with: disposeAction)
21 | }
22 |
23 | func dispose() {
24 | _dispose.dispose()
25 | }
26 |
27 | func asObservable() -> Observable {
28 | return _source
29 | }
30 | }
31 |
32 | /**
33 | Enables monitoring of sequence computation.
34 |
35 | If there is at least one sequence computation in progress, `true` will be sent.
36 | When all activities complete `false` will be sent.
37 | */
38 | public class ActivityIndicator: SharedSequenceConvertibleType {
39 | public typealias Element = Bool
40 | public typealias SharingStrategy = DriverSharingStrategy
41 |
42 | private let _lock = NSRecursiveLock()
43 | private let _variable = BehaviorRelay(value: 0)
44 | private let _loading: SharedSequence
45 |
46 | public init() {
47 | _loading = _variable.asDriver()
48 | .map { $0 > 0 }
49 | .distinctUntilChanged()
50 | }
51 |
52 | fileprivate func trackActivityOfObservable(_ source: O) -> Observable {
53 | return Observable.using({ () -> ActivityToken in
54 | self.increment()
55 | return ActivityToken(source: source.asObservable(), disposeAction: self.decrement)
56 | }, observableFactory: { value in
57 | return value.asObservable()
58 | })
59 | }
60 |
61 | func increment() {
62 | _lock.lock()
63 | _variable.accept(_variable.value + 1)
64 | _lock.unlock()
65 | }
66 |
67 | private func decrement() {
68 | _lock.lock()
69 | _variable.accept(_variable.value - 1)
70 | _lock.unlock()
71 | }
72 |
73 | func stop() {
74 |
75 | }
76 |
77 |
78 | public func asSharedSequence() -> SharedSequence {
79 | return _loading
80 | }
81 | }
82 |
83 | extension ObservableConvertibleType {
84 | public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable {
85 | return activityIndicator.trackActivityOfObservable(self)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/DKVideo/Managers/HUDUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HUDUtils.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/3/20.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Toast_Swift
11 | //
12 | func showLoadHud(inView: UIView = keyWindow) {
13 | inView.makeToastActivity(.center)
14 | }
15 |
16 | func hideAllHud(inView: UIView = keyWindow) {
17 | inView.hideAllToasts(includeActivity: true, clearQueue: true)
18 |
19 | }
20 |
21 | func showMessage(message: String,
22 | inView: UIView = keyWindow,
23 | duration: TimeInterval = 1.5,
24 | position: ToastPosition = .center,
25 | title: String? = nil,
26 | image: UIImage? = nil,
27 | style: ToastStyle = ToastStyle(),
28 | completion: ((Bool) -> Void)? = nil) {
29 |
30 | keyWindow.makeToast(message, duration: duration, position: position, title: title, image:image, style: style, completion: completion)
31 | }
32 |
33 | var keyWindow: UIWindow {
34 | return UIApplication.shared.keyWindow!
35 | }
36 |
--------------------------------------------------------------------------------
/DKVideo/Managers/LibsManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibsManager.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/3/6.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | //import Bugly
10 | @_exported import ChameleonFramework
11 |
12 | @_exported import HandyJSON
13 | import IQKeyboardManagerSwift
14 | @_exported import KafkaRefresh
15 | @_exported import NSObject_Rx
16 | import NVActivityIndicatorView
17 | @_exported import Rswift
18 | @_exported import RxCocoa
19 | @_exported import RxOptional
20 | @_exported import RxSwift
21 | @_exported import SwifterSwift
22 | @_exported import SwiftyJSON
23 | @_exported import SDWebImage
24 |
25 | #if DEBUG
26 | import FLEX
27 | #endif
28 |
29 | /// 配置各框架
30 | class LibsManager: NSObject {
31 | static let shared = LibsManager()
32 |
33 | override init() {
34 | super.init()
35 | self.setBugly()
36 | self.setupActivityView()
37 | self.setupKafkaRefresh()
38 | self.setupKeyboardManager()
39 | // 加载下载项
40 | DispatchQueue.global().async {
41 | print(DownLoadManage.shared)
42 | }
43 | if !UserDefaults.defaultConfig{
44 | UserDefaults.useWKWebview = true
45 | UserDefaults.showVipWebView = true
46 | UserDefaults.isPCAgent = true
47 | UserDefaults.defaultConfig = true
48 | }
49 |
50 | }
51 |
52 | func showFlex() {
53 | #if DEBUG
54 | FLEXManager.shared.showExplorer()
55 | #endif
56 | }
57 |
58 | func setBugly() {
59 | // var config = BuglyConfig()
60 | // config.reportLogLevel = .error
61 | // Bugly.start(withAppId: "8096bc4c87", config: config)
62 | }
63 |
64 | func configTheme() {
65 | var theme = ThemeType.currentTheme()
66 | // theme = theme.toggled()
67 | themeService.switch(theme)
68 |
69 | if #available(iOS 13.0, *) {
70 | globalStatusBarStyle.accept(.default)
71 | } else {
72 | globalStatusBarStyle.accept(.default)
73 | }
74 | }
75 |
76 | func setupKafkaRefresh() {
77 | if let defaults = KafkaRefreshDefaults.standard() {
78 | defaults.headDefaultStyle = .replicatorAllen
79 | defaults.footDefaultStyle = .native
80 | defaults.backgroundColor = .clear
81 | themeService.rx
82 | .bind({ $0.secondary }, to: defaults.rx.themeColor)
83 | .disposed(by: rx.disposeBag)
84 | }
85 | }
86 |
87 | func setupActivityView() {
88 | NVActivityIndicatorView.DEFAULT_TYPE = .ballRotateChase
89 | NVActivityIndicatorView.DEFAULT_COLOR = .secondary()
90 | }
91 |
92 | func setupKeyboardManager() {
93 | IQKeyboardManager.shared.enable = true
94 | IQKeyboardManager.shared.enableAutoToolbar = false
95 | IQKeyboardManager.shared.shouldResignOnTouchOutside = true
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/DKVideo/Managers/LogManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LogManager.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/3/20.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 |
12 | public func logDebug(_ message: @autoclosure () -> String, file: String = #file, function: String = #function, line: Int = #line) {
13 | let text = "\n[\(Date()) Debug] \(file.lastPathComponent.deletingPathExtension).\(function.replacingOccurrences(of: "()", with: "")):\(line)" + message()
14 | #if DEBUG
15 | print(text)
16 | #else
17 | //Log.debug(text)
18 | #endif
19 | }
20 |
21 | public func logError(_ message: @autoclosure () -> String, file: String = #file, function: String = #function, line: Int = #line) {
22 | let text = "\n[\(Date()) Error] \(file.lastPathComponent.deletingPathExtension).\(function.replacingOccurrences(of: "()", with: "")):\(line)" + message()
23 | #if DEBUG
24 | print(text)
25 | #else
26 | //Log.error(text)
27 | #endif
28 | }
29 |
30 | public func logInfo(_ message: @autoclosure () -> String, file: String = #file, function: String = #function, line: Int = #line) {
31 | let text = "\n[\(Date()) Info] \(file.lastPathComponent.deletingPathExtension).\(function.replacingOccurrences(of: "()", with: "")):\(line)" + message()
32 | #if DEBUG
33 | print(text)
34 | #else
35 | // Log.info(text)
36 | #endif
37 | }
38 |
39 | public func logVerbose(_ message: @autoclosure () -> String, file: String = #file, function: String = #function, line: Int = #line) {
40 | let text = "\n[\(Date()) Verbose] \(file.lastPathComponent.deletingPathExtension).\(function.replacingOccurrences(of: "()", with: "")):\(line)" + message()
41 | #if DEBUG
42 | print(text)
43 | #else
44 | //Log.verbose(text)
45 | #endif
46 | }
47 |
48 | public func logWarn(_ message: @autoclosure () -> String, file: String = #file, function: String = #function, line: Int = #line) {
49 | let text = "\n[\(Date()) Warn] \(file.lastPathComponent.deletingPathExtension).\(function.replacingOccurrences(of: "()", with: "")):\(line)" + message()
50 | #if DEBUG
51 | print(text)
52 | #else
53 | // Log.warning(text)
54 | #endif
55 | }
56 |
57 | public func logResourcesCount() {
58 | #if DEBUG
59 | logDebug("RxSwift resources count: \(RxSwift.Resources.total)")
60 | #endif
61 | }
62 |
--------------------------------------------------------------------------------
/DKVideo/Managers/URLIntercept.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLIntercept.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/4.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import SuperPlayer
10 | import SwifterSwift
11 | import RxRelay
12 | let URLInterceptKey = "Intercepted"
13 | /// 网络请求拦截器
14 | class URLIntercept: URLProtocol {
15 | static let videoUrl = BehaviorRelay(value: "")
16 | var newTask: URLSessionTask?
17 | /// 返回是否监控此条网络请求
18 | /// - Parameter request: 网络请求
19 | override class func canInit(with request: URLRequest) -> Bool {
20 | print("--caninit--" + (request.url?.absoluteString ?? ""))
21 | // 如果是已经拦截过的就放行,避免出现死循环
22 | if URLProtocol.property(forKey: URLInterceptKey, in: request) as? Bool ?? false {
23 | return false
24 | }
25 | if request.allHTTPHeaderFields.isNilOrEmpty {
26 | print("##########\(request.description)")
27 | return false
28 | }
29 | // 不是网络请求,不处理
30 | if let urlScheme = request.url?.scheme?.lowercased() {
31 | if ["http", "https", "ftp"].contains(urlScheme) {
32 | return true
33 | }
34 | }
35 |
36 | // 不拦截其他
37 | return false
38 | }
39 |
40 | /// 设置我们自己的自定义请求
41 | /// - Parameter request: 当前的网络请求
42 | override class func canonicalRequest(for request: URLRequest) -> URLRequest {
43 | var mutableReqeust: URLRequest = request
44 | guard let urlStr = request.url?.absoluteString else { return request }
45 | // 视频播放拦截
46 | print("+++++++++++++" + urlStr.pathExtension)
47 | if urlStr.pathExtension.hasPrefix("m3u8") && !urlStr.contains("jx.688ing"){
48 | // mutableReqeust.url = nil
49 | print("=========video=======\(urlStr)")
50 | videoUrl.accept(urlStr)
51 | }
52 | return mutableReqeust
53 | }
54 |
55 | override func startLoading() {
56 | // 给我们处理过的请求设置一个标识符, 防止无限循环,
57 | var request = self.request
58 | URLProtocol.setProperty(true, forKey: URLInterceptKey, in: request as! NSMutableURLRequest)
59 |
60 | // 广告拦截标识字符
61 | var isAD = false
62 | adString.forEach { str in
63 | if (request.url?.absoluteString ?? "").contains(str) { isAD = true; return }
64 | }
65 | let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
66 | if UserDefaults.isPCAgent {
67 | request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Safari/605.1.15", forHTTPHeaderField: "User-Agent")
68 | } else {
69 | request.setValue("Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", forHTTPHeaderField: "User-Agent")
70 | }
71 | if isAD {} else {
72 | self.newTask = session.dataTask(with: request)
73 | print("====REQUEST:====\(request.url?.absoluteString ?? "")")
74 | self.newTask?.resume()
75 | }
76 | }
77 |
78 | override func stopLoading() {
79 | self.newTask?.cancel()
80 | }
81 | }
82 |
83 | extension URLIntercept: URLSessionDelegate, URLSessionDataDelegate {
84 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
85 | client?.urlProtocol(self, didLoad: data)
86 | }
87 |
88 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
89 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
90 | completionHandler(.allow)
91 | }
92 |
93 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
94 | client?.urlProtocolDidFinishLoading(self)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/BaseTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseTableViewCell.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/6.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class BaseTableViewCell: TableViewCell {
12 | var viewModel = TableCellViewModel()
13 |
14 | func bindViewModel(viewModel: TableCellViewModel) {
15 | self.viewModel = viewModel
16 |
17 | viewModel.title.asDriver().drive(titleLabel.rx.text).disposed(by: rx.disposeBag)
18 | viewModel.title.asDriver().replaceNilWith("").map { $0.isEmpty }.drive(titleLabel.rx.isHidden).disposed(by: rx.disposeBag)
19 |
20 | viewModel.detail.asDriver().drive(detailLabel.rx.text).disposed(by: rx.disposeBag)
21 | viewModel.detail.asDriver().replaceNilWith("").map { $0.isEmpty }.drive(detailLabel.rx.isHidden).disposed(by: rx.disposeBag)
22 |
23 | viewModel.hidesDisclosure.asDriver().drive(rightImageView.rx.isHidden).disposed(by: rx.disposeBag)
24 |
25 | viewModel.image.asDriver().filterNil()
26 | .drive(leftImageView.rx.image).disposed(by: rx.disposeBag)
27 | }
28 |
29 | lazy var leftImageView: UIImageView = {
30 | let view = UIImageView(frame: CGRect())
31 | view.contentMode = .scaleAspectFit
32 | view.snp.makeConstraints { make in
33 | make.size.equalTo(50)
34 | }
35 | return view
36 | }()
37 |
38 | lazy var textsStackView: UIStackView = {
39 | let views: [UIView] = [self.titleLabel, self.detailLabel]
40 | let view = UIStackView(arrangedSubviews: views)
41 | view.spacing = 2
42 | return view
43 | }()
44 |
45 | let titleLabel: UILabel = UILabel(fontSize: 14, text: "")
46 |
47 | let detailLabel: UILabel = UILabel(fontSize: 12, text: "")
48 |
49 | lazy var rightImageView: UIImageView = {
50 | let view = UIImageView(frame: CGRect())
51 | view.image = R.image.icon_cell_disclosure()?.template
52 | view.snp.makeConstraints { make in
53 | make.width.equalTo(20)
54 | }
55 | return view
56 | }()
57 |
58 | override func makeUI() {
59 | super.makeUI()
60 |
61 | themeService.rx
62 | .bind({ $0.text }, to: titleLabel.rx.textColor)
63 | .bind({ $0.textGray }, to: detailLabel.rx.textColor)
64 | .bind({ $0.secondary }, to: [leftImageView.rx.tintColor, rightImageView.rx.tintColor])
65 | .disposed(by: rx.disposeBag)
66 |
67 | stackView.addArrangedSubview(leftImageView)
68 | stackView.addArrangedSubview(textsStackView)
69 | stackView.addArrangedSubview(rightImageView)
70 | stackView.snp.remakeConstraints { make in
71 | let inset: CGFloat = 15
72 | make.edges.equalToSuperview().inset(UIEdgeInsets(top: inset / 2, left: inset, bottom: inset / 2, right: inset))
73 | make.height.greaterThanOrEqualTo(45)
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/CollectionFlowLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionFlowLayout.swift
3 | //
4 | //
5 | // Created by 朱德坤 on 2019/4/4.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum AlignType : NSInteger {
12 | case left = 0
13 | case center = 1
14 | case right = 2
15 | }
16 | class CollectionFlowLayout: UICollectionViewFlowLayout {
17 | //两个Cell之间的距离
18 | var betweenOfCell : CGFloat{
19 | didSet{
20 | self.minimumInteritemSpacing = betweenOfCell
21 | }
22 | }
23 | //cell对齐方式
24 | var cellType : AlignType = AlignType.center
25 | //在居中对齐的时候需要知道这行所有cell的宽度总和
26 | var sumCellWidth : CGFloat = 0.0
27 |
28 | override init() {
29 | betweenOfCell = 5.0
30 | super.init()
31 | scrollDirection = .vertical
32 | minimumLineSpacing = 5
33 | sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
34 | }
35 | convenience init(_ cellType:AlignType){
36 | self.init()
37 | self.cellType = cellType
38 | }
39 | convenience init(_ cellType: AlignType, _ betweenOfCell: CGFloat){
40 | self.init()
41 | self.cellType = cellType
42 | self.betweenOfCell = betweenOfCell
43 | }
44 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
45 |
46 | let layoutAttributes_super : [UICollectionViewLayoutAttributes] = super.layoutAttributesForElements(in: rect) ?? [UICollectionViewLayoutAttributes]()
47 | let layoutAttributes:[UICollectionViewLayoutAttributes] = NSArray(array: layoutAttributes_super, copyItems:true)as! [UICollectionViewLayoutAttributes]
48 | var layoutAttributes_t : [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()
49 | for index in 0.. CommonListData {
19 | return .init(id: id, text: text, selected: selected)
20 | }
21 | }
22 |
23 | /// 默认的列表协议数据实现
24 | struct CommonListData: ListAble {
25 | var icon: UIImage?
26 |
27 | var text: String
28 |
29 | var id: String
30 |
31 | var selected: Bool
32 | init(id: String = "", text: String = "", selected: Bool = false,icon:UIImage? = nil) {
33 | self.id = id
34 | self.text = text
35 | self.selected = selected
36 | self.icon = icon
37 | }
38 | }
39 |
40 | /// 通用列表选择界面
41 | class CommonSelectVC: ViewController, UITableViewDelegate, UITableViewDataSource {
42 | /// 是否可以多选
43 | var shouldMutableSelect = false
44 | let tableView = UITableView(frame: UIScreen.main.bounds, style: .plain)
45 | /// 列表数据源
46 | var listDataProvider: ((inout [T]) -> Void)!
47 | /// 列表数据
48 | var listData = [T]() {
49 | didSet {
50 | tableView.reloadData()
51 | }
52 | }
53 |
54 | /// 选择完成的回调
55 | var commitHandle: (([T]) -> Void)!
56 | /// 返回时是否需要动画
57 | var animate = true
58 | override func viewDidLoad() {
59 | super.viewDidLoad()
60 |
61 | let bgView = UIVisualEffectView(frame: view.frame)
62 | bgView.effect = UIBlurEffect(style: .dark)
63 | bgView.alpha = 0.5
64 | bgView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismissSelect)))
65 | view.addSubviews([bgView, tableView])
66 | tableView.dataSource = self
67 | tableView.delegate = self
68 | tableView.tableFooterView = UIView()
69 | tableView.showsVerticalScrollIndicator = false
70 | tableView.snp.makeConstraints { $0.edges.equalTo(UIEdgeInsets.zero) }
71 | if shouldMutableSelect {
72 | let item = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(commit))
73 | navigationItem.setRightBarButton(item, animated: true)
74 | }
75 | themeService.rx
76 | .bind({ $0.background }, to: tableView.rx.backgroundColor)
77 | .disposed(by: rx.disposeBag)
78 | }
79 |
80 | override func viewWillAppear(_ animated: Bool) {
81 | super.viewWillAppear(animated)
82 | listDataProvider?(&listData)
83 | }
84 |
85 | @objc func commit() {
86 | let selectedList = (listData.filter { $0.selected })
87 | commitHandle(selectedList)
88 | dismissSelect()
89 | }
90 |
91 | @objc func dismissSelect() {
92 | dismiss(animated: true)
93 | }
94 |
95 | public convenience init(title: String = "请选择", shouldMutableSelect: Bool = false, listDataProvider: @escaping (inout [T]) -> Void, commitHandle: @escaping (([T]) -> Void)) {
96 | self.init()
97 | // self.init(title: title)
98 | self.title = title
99 | self.shouldMutableSelect = shouldMutableSelect
100 | self.listDataProvider = listDataProvider
101 | self.commitHandle = commitHandle
102 | }
103 |
104 | func showSelect(in vc: UIViewController, frame: CGRect = CGRect(x: 20, y: 60, width: screenWidth - 40, height: screenHeight - 120)) {
105 | tableView.frame = frame
106 | view.backgroundColor = .clear
107 | modalPresentationStyle = .custom
108 | vc.present(self, animated: false) {}
109 | }
110 |
111 | func numberOfSections(in tableView: UITableView) -> Int {
112 | return 1 // listData.count
113 | }
114 |
115 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
116 | return listData.count
117 | }
118 |
119 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
120 | var cell = tableView.dequeueReusableCell(withIdentifier: "CommonSelectCell")
121 | if cell == nil {
122 | cell = UITableViewCell(style: .default, reuseIdentifier: "CommonSelectCell")
123 | }
124 | cell?.textLabel?.text = listData[indexPath.row].text
125 | // cell?.imageView?.image = listData[indexPath.row].icon
126 | cell?.accessoryView = listData[indexPath.row].selected ? UIImageView(image: R.image.icon_common_select()) : nil
127 | cell?.textLabel?.textColor = listData[indexPath.row].selected ? .flatBlue : .darkGray
128 | cell?.textLabel?.font = UIFont.systemFont(ofSize: 13)
129 | cell?.backgroundColor = UIColor.clear
130 | cell?.textLabel?.textColor = .text()
131 | cell?.imageView?.image = listData[indexPath.row].icon
132 | cell?.imageView?.contentMode = .center
133 | return cell!
134 | }
135 |
136 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
137 | tableView.deselectRow(at: indexPath, animated: true)
138 | let cell = tableView.cellForRow(at: indexPath)
139 | if !shouldMutableSelect {
140 | for i in 0..(inVC: UIViewController, title: String = "请选择", isMutableSelect: Bool = false, height: CGFloat = screenHeight - 200, listDataProvider: @escaping ((inout [T]) -> Void), commitHandle: @escaping (([T]) -> Void)) {
164 | let alert = UIAlertController(title: title, message: "", preferredStyle: .alert)
165 | let vc = CommonSelectVC(listDataProvider: listDataProvider, commitHandle: commitHandle)
166 | alert.setValue(vc, forKey: "contentViewController")
167 |
168 | vc.preferredContentSize.height = height
169 | alert.preferredContentSize.height = height
170 | vc.shouldMutableSelect = isMutableSelect
171 | if isMutableSelect {
172 | alert.addAction(title: "确定", style: .default, isEnabled: true) { _ in
173 | vc.commit()
174 | }
175 | } else {
176 | alert.addAction(title: "取消", style: .cancel, isEnabled: true, handler: nil)
177 | }
178 |
179 | inVC.present(alert, animated: true, completion: nil)
180 | }
181 |
182 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/NavigationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationController.swift
3 | // szwhExpressway
4 | //
5 | // Created by 朱德坤 on 2019/3/20.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AttributedLib
11 |
12 | class NavigationController: UINavigationController {
13 |
14 | override var preferredStatusBarStyle: UIStatusBarStyle {
15 | return globalStatusBarStyle.value
16 | }
17 |
18 |
19 | override func viewDidLoad() {
20 | super.viewDidLoad()
21 |
22 | // Do any additional setup after loading the view.
23 | // hero.isEnabled = true
24 | // hero.navigationAnimationType = .fade
25 | // hero.modalAnimationType = .autoReverse(presenting: .fade)
26 | // hero.navigationAnimationType = .autoReverse(presenting: .slide(direction: .left))
27 | if #available(iOS 13.0, *) {
28 | if self.modalPresentationStyle == .pageSheet{
29 | self.modalPresentationStyle = .fullScreen
30 | }
31 | overrideUserInterfaceStyle = .light
32 | }
33 | navigationBar.isTranslucent = false
34 | navigationBar.backIndicatorImage = R.image.icon_navigation_back()
35 | navigationBar.backIndicatorTransitionMaskImage = R.image.icon_navigation_back()
36 |
37 | themeService.rx
38 | .bind({ $0.text }, to: navigationBar.rx.tintColor)
39 | .bind({ $0.primary}, to: navigationBar.rx.barTintColor)
40 | .bind({ [NSAttributedString.Key.foregroundColor: $0.text] }, to: navigationBar.rx.titleTextAttributes)
41 | .disposed(by: rx.disposeBag)
42 | }
43 | override func pushViewController(_ viewController: UIViewController, animated: Bool) {
44 | if (self.children.count==1) {
45 | viewController.hidesBottomBarWhenPushed = true
46 | }
47 | super.pushViewController(viewController, animated: animated)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/PageViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageViewController.swift
3 | // szwhExpressway
4 | //
5 | // Created by 朱德坤 on 2019/4/10.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import XLPagerTabStrip
11 | class PageViewController: ButtonBarPagerTabStripViewController {
12 | /// viewControllers
13 | var pages = [UIViewController]()
14 | var automaticallyAdjustsLeftBarButtonItem = true
15 | lazy var barTitleColor = UIColor.textGray()
16 | lazy var selectTitleColor = UIColor.secondary()
17 | lazy var backBarButton: UIBarButtonItem = {
18 | let view = UIBarButtonItem()
19 | view.title = ""
20 | return view
21 | }()
22 |
23 | lazy var closeBarButton: UIBarButtonItem = {
24 | let view = UIBarButtonItem(image: R.image.icon_navigation_back(),
25 | style: .plain,
26 | target: self,
27 | action: nil)
28 | return view
29 | }()
30 |
31 | var navigationTitle = "" {
32 | didSet { navigationItem.title = navigationTitle }
33 | }
34 |
35 | var hasRedPointIndexs = [Int]() {
36 | didSet {
37 | buttonBarView.reloadData()
38 | }
39 | }
40 |
41 | lazy var setting: ButtonBarPagerTabStripSettings = {
42 | settings.style.buttonBarBackgroundColor = .primary()
43 | settings.style.buttonBarHeight = 40
44 | settings.style.buttonBarItemBackgroundColor = .primary()
45 | settings.style.selectedBarBackgroundColor = selectTitleColor
46 | settings.style.buttonBarItemFont = .systemFont(ofSize: 15)
47 | settings.style.selectedBarHeight = 2.0
48 | settings.style.buttonBarMinimumLineSpacing = 0
49 | settings.style.buttonBarItemTitleColor = barTitleColor
50 | settings.style.buttonBarItemsShouldFillAvailableWidth = true
51 | settings.style.buttonBarLeftContentInset = 15
52 | settings.style.buttonBarRightContentInset = 15
53 | var setting = settings
54 | return setting
55 | }()
56 |
57 | override func viewDidLoad() {
58 | // settings 设置要在 viewDidLoad前设置
59 | settings = setting
60 |
61 | changeCurrentIndexProgressive = { [weak self] (oldCell: ButtonBarViewCell?, newCell: ButtonBarViewCell?, _: CGFloat, changeCurrentIndex: Bool, _: Bool) -> Void in
62 | guard changeCurrentIndex == true else { return }
63 | oldCell?.label.textColor = self?.barTitleColor
64 | newCell?.label.textColor = self?.selectTitleColor
65 | }
66 |
67 | super.viewDidLoad()
68 | makeUI()
69 | }
70 |
71 | func makeUI() {
72 | // hero.isEnabled = true
73 | navigationItem.backBarButtonItem = backBarButton
74 | closeBarButton.rx.tap.bind { [weak self] () in
75 | self?.dismiss(animated: true, completion: nil)
76 | }.disposed(by: rx.disposeBag)
77 | themeService.rx
78 | .bind({ $0.background }, to: view.rx.backgroundColor)
79 | .bind({ $0.primary }, to: buttonBarView.rx.backgroundColor)
80 | .bind({ $0.secondaryDark }, to: [backBarButton.rx.tintColor, closeBarButton.rx.tintColor])
81 | .disposed(by: rx.disposeBag)
82 | themeService.attrsStream.bind { [unowned self] theme in
83 | self.settings.style.buttonBarItemBackgroundColor = theme.primary
84 | self.barTitleColor = theme.textGray
85 | self.selectTitleColor = theme.secondary
86 | self.settings.style.selectedBarBackgroundColor = self.selectTitleColor
87 | self.settings.style.buttonBarItemTitleColor = self.barTitleColor
88 | self.buttonBarView.reloadData()
89 | }.disposed(by: rx.disposeBag)
90 | buttonBarView.shadowOffset = CGSize(width: 0, height: 0.3)
91 | buttonBarView.shadowOpacity = 0.3
92 | buttonBarView.shadowRadius = 0.3
93 | buttonBarView.clipsToBounds = false
94 | }
95 |
96 | public override func viewWillAppear(_ animated: Bool) {
97 | super.viewWillAppear(animated)
98 |
99 | if automaticallyAdjustsLeftBarButtonItem {
100 | adjustLeftBarButtonItem()
101 | }
102 | }
103 |
104 | public override func viewDidAppear(_ animated: Bool) {
105 | super.viewDidAppear(animated)
106 | logResourcesCount()
107 | }
108 |
109 | deinit {
110 | logDebug("\(type(of: self)): Deinited")
111 | logResourcesCount()
112 | }
113 |
114 | func adjustLeftBarButtonItem() {
115 | if navigationController?.viewControllers.count ?? 0 > 1 { // Pushed
116 | navigationItem.leftBarButtonItem = nil
117 | } else if presentingViewController != nil { // presented
118 | navigationItem.leftBarButtonItem = closeBarButton
119 | }
120 | }
121 |
122 | @objc func closeAction(sender: AnyObject) {
123 | dismiss(animated: true, completion: nil)
124 | }
125 |
126 | override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
127 | return pages
128 | }
129 |
130 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
131 | let cell = super.collectionView(collectionView, cellForItemAt: indexPath)
132 | guard let label = (cell as? ButtonBarViewCell)?.label else { return cell }
133 | if let redPoint = cell.contentView.viewWithTag(10010) {
134 | redPoint.isHidden = !hasRedPointIndexs.contains(indexPath.row)
135 | } else {
136 | let contenntV = cell.contentView
137 | let redPoint = UIView()
138 | redPoint.tag = 10010
139 | contenntV.addSubview(redPoint)
140 | redPoint.snp.makeConstraints { make in
141 | make.centerY.equalTo(label.snp.top)
142 | make.centerX.equalTo(label.snp.right)
143 | make.width.height.equalTo(10)
144 | }
145 | redPoint.cornerRadius = 5
146 | redPoint.backgroundColor = .red
147 | redPoint.isHidden = !hasRedPointIndexs.contains(indexPath.row)
148 | }
149 | cell.clipsToBounds = false
150 | return cell
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/PageViewController2.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageViewController2.swift
3 | // szwhExpressway
4 | //
5 | // Created by 朱德坤 on 2019/8/20.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import XLPagerTabStrip
11 | class PageViewController2: ButtonBarPagerTabStripViewController {
12 | /// viewControllers
13 | var pages = [UIViewController]()
14 | var automaticallyAdjustsLeftBarButtonItem = true
15 | lazy var barTitleColor = themeService.type.associatedObject.textGray
16 | lazy var selectTitleColor = UIColor.primary()
17 | lazy var backBarButton: UIBarButtonItem = {
18 | let view = UIBarButtonItem()
19 | view.title = ""
20 | return view
21 | }()
22 | var hasRedPointIndexs = [Int](){
23 | didSet{
24 | buttonBarView.reloadData()
25 | }
26 | }
27 |
28 | lazy var closeBarButton: UIBarButtonItem = {
29 | let view = UIBarButtonItem(image: R.image.icon_navigation_back(),
30 | style: .plain,
31 | target: self,
32 | action: nil)
33 | return view
34 | }()
35 |
36 | var navigationTitle = "" {
37 | didSet { navigationItem.title = navigationTitle }
38 | }
39 |
40 | lazy var setting : ButtonBarPagerTabStripSettings = {
41 | settings.style.buttonBarBackgroundColor = .primary()
42 | settings.style.buttonBarHeight = 40
43 | settings.style.buttonBarItemBackgroundColor = .primary()
44 | settings.style.selectedBarBackgroundColor = .clear
45 | settings.style.buttonBarItemFont = .systemFont(ofSize: 15)
46 | settings.style.selectedBarHeight = 0
47 | settings.style.buttonBarMinimumLineSpacing = 20
48 | settings.style.buttonBarItemTitleColor = barTitleColor
49 | settings.style.buttonBarItemsShouldFillAvailableWidth = false
50 | settings.style.buttonBarLeftContentInset = 15
51 | settings.style.buttonBarRightContentInset = 15
52 | var setting = settings
53 | return setting
54 | }()
55 | override func viewDidLoad() {
56 | // settings 设置要在 viewDidLoad前设置
57 | settings = setting
58 | changeCurrentIndexProgressive = { [weak self] (oldCell: ButtonBarViewCell?, newCell: ButtonBarViewCell?, _: CGFloat, changeCurrentIndex: Bool, _: Bool) -> Void in
59 | guard changeCurrentIndex == true else { return }
60 | oldCell?.label.textColor = self?.barTitleColor
61 | oldCell?.contentView.viewWithTag(10086)?.backgroundColor = UIColor(hex: 0xe6e6e6)
62 | newCell?.label.textColor = self?.selectTitleColor
63 | newCell?.contentView.viewWithTag(10086)?.backgroundColor = .secondary()
64 | }
65 |
66 | super.viewDidLoad()
67 | makeUI()
68 | }
69 |
70 | func makeUI() {
71 | // hero.isEnabled = true
72 | navigationItem.backBarButtonItem = backBarButton
73 | closeBarButton.rx.tap.bind { [weak self] () in
74 | self?.dismiss(animated: true, completion: nil)
75 | }.disposed(by: rx.disposeBag)
76 | themeService.rx
77 | .bind({ $0.background }, to: view.rx.backgroundColor)
78 | .bind({ $0.primary }, to: buttonBarView.rx.backgroundColor)
79 | .bind({ $0.secondaryDark }, to: [backBarButton.rx.tintColor, closeBarButton.rx.tintColor])
80 | .disposed(by: rx.disposeBag)
81 | themeService.attrsStream.bind { [unowned self] theme in
82 | self.settings.style.buttonBarItemBackgroundColor = theme.primary
83 | self.barTitleColor = theme.textGray
84 | self.selectTitleColor = theme.primary
85 | self.settings.style.buttonBarItemTitleColor = self.barTitleColor
86 | self.buttonBarView.reloadData()
87 | }.disposed(by: rx.disposeBag)
88 | buttonBarView.shadowOffset = CGSize(width: 0, height: 0.3)
89 | buttonBarView.shadowOpacity = 0.3
90 | buttonBarView.shadowRadius = 0.3
91 | buttonBarView.clipsToBounds = false
92 | }
93 |
94 | public override func viewWillAppear(_ animated: Bool) {
95 | super.viewWillAppear(animated)
96 |
97 | if automaticallyAdjustsLeftBarButtonItem {
98 | adjustLeftBarButtonItem()
99 | }
100 | }
101 |
102 | public override func viewDidAppear(_ animated: Bool) {
103 | super.viewDidAppear(animated)
104 | logResourcesCount()
105 | }
106 |
107 | deinit {
108 | logDebug("\(type(of: self)): Deinited")
109 | logResourcesCount()
110 | }
111 |
112 | func adjustLeftBarButtonItem() {
113 | if navigationController?.viewControllers.count ?? 0 > 1 { // Pushed
114 | navigationItem.leftBarButtonItem = nil
115 | } else if presentingViewController != nil { // presented
116 | navigationItem.leftBarButtonItem = closeBarButton
117 | }
118 | }
119 |
120 | @objc func closeAction(sender: AnyObject) {
121 | dismiss(animated: true, completion: nil)
122 | }
123 |
124 | override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
125 | return pages
126 | }
127 |
128 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
129 | let cell = super.collectionView(collectionView, cellForItemAt: indexPath)
130 | if let bgview = cell.contentView.viewWithTag(10086) {
131 | bgview.backgroundColor = indexPath.row == currentIndex ? .secondary(): UIColor(hex: 0xe6e6e6)
132 | } else {
133 | let contenntV = cell.contentView
134 | let redPoint = UIView()
135 | let bgview = UIView()
136 | bgview.tag = 10086
137 | redPoint.tag = 10010
138 | contenntV.insertSubview(bgview, at: 0)
139 | contenntV.addSubview(redPoint)
140 | bgview.backgroundColor = indexPath.row == currentIndex ? .secondary(): UIColor(hex: 0xe6e6e6)
141 | bgview.snp.makeConstraints { make in
142 | make.edges.equalTo(UIEdgeInsets(top: 5, left: 0, bottom: 5, right: 0))
143 | }
144 | redPoint.snp.makeConstraints { (make) in
145 | make.centerY.equalTo(bgview.snp.top)
146 | make.centerX.equalTo(bgview.snp.right)
147 | make.width.height.equalTo(10)
148 | }
149 | redPoint.cornerRadius = 5
150 | redPoint.backgroundColor = .red
151 | bgview.cornerRadius = 3
152 | redPoint.isHidden = !hasRedPointIndexs.contains(indexPath.row)
153 | }
154 | if let redPoint = cell.contentView.viewWithTag(10010){
155 | redPoint.isHidden = !hasRedPointIndexs.contains(indexPath.row)
156 | }
157 | cell.clipsToBounds = false
158 | return cell
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/TableCellViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableCellViewModel.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/6.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | class TableCellViewModel:NSObject{
11 | let title = BehaviorRelay(value: nil)
12 | let detail = BehaviorRelay(value: nil)
13 | let image = BehaviorRelay(value: nil)
14 | let hidesDisclosure = BehaviorRelay(value: false)
15 | }
16 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/TableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableViewCell.swift
3 | // szwhExpressway
4 | //
5 | // Created by 朱德坤 on 2019/3/25.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 | import UIKit
11 | class TableViewCell: UITableViewCell {
12 | var disposeBag = DisposeBag()
13 |
14 | static var identifier: String {
15 | return String(describing: self)
16 | }
17 |
18 | var isSelection = false
19 | var selectionColor: UIColor? {
20 | didSet {
21 | setSelected(isSelected, animated: true)
22 | }
23 | }
24 |
25 | lazy var containerView: UIView = {
26 | let view = UIView()
27 | view.backgroundColor = .clear
28 | self.addSubview(view)
29 | view.snp.makeConstraints { make in
30 | make.edges.equalToSuperview()
31 | }
32 | return view
33 | }()
34 |
35 | lazy var stackView: UIStackView = {
36 | let subviews: [UIView] = []
37 | let view = UIStackView(arrangedSubviews: subviews)
38 | view.axis = .horizontal
39 | view.alignment = .center
40 | self.containerView.addSubview(view)
41 | view.snp.makeConstraints { make in
42 | make.edges.equalToSuperview()
43 | }
44 | return view
45 | }()
46 |
47 |
48 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
49 | super.init(style: .value2, reuseIdentifier: reuseIdentifier)
50 | makeUI()
51 | }
52 |
53 | required init?(coder aDecoder: NSCoder) {
54 | super.init(coder: aDecoder)
55 | makeUI()
56 | }
57 |
58 | override func prepareForReuse() {
59 | super.prepareForReuse()
60 | disposeBag = DisposeBag()
61 | }
62 |
63 | override func setSelected(_ selected: Bool, animated: Bool) {
64 | super.setSelected(selected, animated: animated)
65 | backgroundColor = selected ? selectionColor : .clear
66 | }
67 |
68 | func makeUI() {
69 | layer.masksToBounds = true
70 | selectionStyle = .none
71 | backgroundColor = .clear
72 |
73 | themeService.rx
74 | .bind({ $0.primaryDark }, to: rx.selectionColor)
75 | .bind({ $0.textGray }, to: textLabel!.rx.textColor)
76 | .bind({ $0.text }, to: detailTextLabel!.rx.textColor)
77 | .disposed(by: rx.disposeBag)
78 |
79 | updateUI()
80 | }
81 |
82 | func updateUI() {
83 | setNeedsDisplay()
84 | }
85 | }
86 |
87 | extension Reactive where Base: TableViewCell {
88 | var selectionColor: Binder {
89 | return Binder(base) { view, attr in
90 | view.selectionColor = attr
91 | }
92 | }
93 | }
94 |
95 | class TableViewHeaderFooter: UITableViewHeaderFooterView {
96 | var disposeBag = DisposeBag()
97 | let bgView = UIView()
98 | override init(reuseIdentifier: String?) {
99 | super.init(reuseIdentifier: reuseIdentifier)
100 | makeUI()
101 | }
102 |
103 | required init?(coder aDecoder: NSCoder) {
104 | fatalError("init(coder:) has not been implemented")
105 | }
106 |
107 | override func prepareForReuse() {
108 | super.prepareForReuse()
109 | disposeBag = DisposeBag()
110 | }
111 |
112 | func makeUI() {
113 | contentView.addSubview(bgView)
114 | bgView.snp.makeConstraints { $0.edges.equalToSuperview() }
115 | themeService.rx
116 | .bind({ $0.primary }, to: bgView.rx.backgroundColor)
117 | .disposed(by: rx.disposeBag)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/TableViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableViewController.swift
3 | // szwhExpressway
4 | //
5 | // Created by 朱德坤 on 2019/3/22.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 | import KafkaRefresh
13 |
14 | class TableViewController: ViewController, UIScrollViewDelegate {
15 |
16 | let headerRefreshTrigger = PublishSubject()
17 | let footerRefreshTrigger = PublishSubject()
18 |
19 | let isHeaderLoading = BehaviorRelay(value: false)
20 | let isFooterLoading = BehaviorRelay(value: false)
21 |
22 | lazy var tableView: UITableView = {
23 | let view = UITableView(frame: CGRect(), style: .plain)
24 | view.emptyDataSetSource = self
25 | view.emptyDataSetDelegate = self
26 | view.rowHeight = UITableView.automaticDimension
27 | view.estimatedRowHeight = 50
28 | view.tableFooterView = UIView()
29 | view.rx.setDelegate(self).disposed(by: rx.disposeBag)
30 | view.cellLayoutMarginsFollowReadableWidth = false
31 | return view
32 | }()
33 |
34 | var clearsSelectionOnViewWillAppear = true
35 |
36 | override func viewDidLoad() {
37 | super.viewDidLoad()
38 | }
39 |
40 | override func viewWillAppear(_ animated: Bool) {
41 | super.viewWillAppear(animated)
42 |
43 | if clearsSelectionOnViewWillAppear == true {
44 | deselectSelectedRow()
45 | }
46 | }
47 |
48 | override func makeUI() {
49 | super.makeUI()
50 |
51 | stackView.spacing = 0
52 | stackView.insertArrangedSubview(tableView, at: 0)
53 | // tableView.snp.makeConstraints{ $0.edges.equalToSuperview()}
54 |
55 | tableView.bindGlobalStyle(forHeadRefreshHandler: { [weak self] in
56 | self?.headerRefreshTrigger.onNext(())
57 | self?.tableView.footRefreshControl.resumeRefreshAvailable()
58 | })
59 |
60 | tableView.bindGlobalStyle(forFootRefreshHandler: { [weak self] in
61 | self?.footerRefreshTrigger.onNext(())
62 | })
63 | tableView.footRefreshControl.setAlertBackgroundColor( themeService.type.associatedObject.background)
64 |
65 | isHeaderLoading.bind(to: tableView.headRefreshControl.rx.isAnimating).disposed(by: rx.disposeBag)
66 | isFooterLoading.bind(to: tableView.footRefreshControl.rx.isAnimating).disposed(by: rx.disposeBag)
67 |
68 | tableView.footRefreshControl.autoRefreshOnFoot = true
69 |
70 | let updateEmptyDataSet = Observable.of(isLoading.mapToVoid().asObservable(), emptyDataSetImageTintColor.mapToVoid()).merge()
71 | updateEmptyDataSet.subscribe(onNext: { [weak self] () in
72 | self?.tableView.reloadEmptyDataSet()
73 | }).disposed(by: rx.disposeBag)
74 | themeService
75 | .rx
76 | .bind({ $0.background }, to: tableView.rx.backgroundColor)
77 | .disposed(by: rx.disposeBag)
78 | }
79 |
80 | override func updateUI() {
81 | super.updateUI()
82 | }
83 | }
84 |
85 | extension TableViewController {
86 |
87 | func deselectSelectedRow() {
88 | if let selectedIndexPaths = tableView.indexPathsForSelectedRows {
89 | selectedIndexPaths.forEach({ (indexPath) in
90 | tableView.deselectRow(at: indexPath, animated: false)
91 | })
92 | }
93 | }
94 | }
95 |
96 | extension TableViewController: UITableViewDelegate {
97 |
98 | func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
99 | if let view = view as? UITableViewHeaderFooterView {
100 | view.textLabel?.font = UIFont(name: ".SFUIText-Bold", size: 15.0)!
101 | themeService.rx
102 | .bind({ $0.text }, to: view.textLabel!.rx.textColor)
103 | .bind({ $0.primaryDark }, to: view.contentView.rx.backgroundColor)
104 | .disposed(by: rx.disposeBag)
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/TestWebVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestWebVC.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/19.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import WebKit
11 | class TestWebVC: ViewController, UIWebViewDelegate {
12 | let webView = UIWebView()
13 | override func makeUI() {
14 | super.makeUI()
15 | view.addSubview(webView)
16 | webView.snp.makeConstraints { $0.edges.equalToSuperview() }
17 | webView.loadRequest(URLRequest(urlString: "https://jx.688ing.com/")!)
18 | webView.delegate = self
19 | webView.allowsInlineMediaPlayback = true
20 | webView.mediaPlaybackRequiresUserAction = true
21 | }
22 |
23 |
24 |
25 | func webView(_ webView: UIWebView, didFailLoadWithError error: Error) {
26 | print(error)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/TextView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextView.swift
3 | // dataCenter
4 | //
5 | // Created by 朱德坤 on 2019/10/8.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @IBDesignable
12 | open class TextView: UITextView {
13 |
14 | private struct Constants {
15 | static let defaultiOSPlaceholderColor = UIColor(red: 0.0, green: 0.0, blue: 0.0980392, alpha: 0.22)
16 | }
17 |
18 | public let placeholderLabel: UILabel = UILabel()
19 |
20 | private var placeholderLabelConstraints = [NSLayoutConstraint]()
21 |
22 | @IBInspectable open var placeholder: String = "" {
23 | didSet {
24 | placeholderLabel.text = placeholder
25 | }
26 | }
27 |
28 | @IBInspectable open var placeholderColor: UIColor = TextView.Constants.defaultiOSPlaceholderColor {
29 | didSet {
30 | placeholderLabel.textColor = placeholderColor
31 | }
32 | }
33 |
34 | override open var font: UIFont! {
35 | didSet {
36 | if placeholderFont == nil {
37 | placeholderLabel.font = font
38 | }
39 | }
40 | }
41 |
42 | open var placeholderFont: UIFont? {
43 | didSet {
44 | let font = (placeholderFont != nil) ? placeholderFont : self.font
45 | placeholderLabel.font = font
46 | }
47 | }
48 |
49 | override open var textAlignment: NSTextAlignment {
50 | didSet {
51 | placeholderLabel.textAlignment = textAlignment
52 | }
53 | }
54 |
55 | override open var text: String! {
56 | didSet {
57 | textDidChange()
58 | }
59 | }
60 |
61 | override open var attributedText: NSAttributedString! {
62 | didSet {
63 | textDidChange()
64 | }
65 | }
66 |
67 | override open var textContainerInset: UIEdgeInsets {
68 | didSet {
69 | updateConstraintsForPlaceholderLabel()
70 | }
71 | }
72 |
73 | override public init(frame: CGRect, textContainer: NSTextContainer?) {
74 | super.init(frame: frame, textContainer: textContainer)
75 | commonInit()
76 | }
77 |
78 | required public init?(coder aDecoder: NSCoder) {
79 | super.init(coder: aDecoder)
80 | commonInit()
81 | }
82 |
83 | private func commonInit() {
84 | #if swift(>=4.2)
85 | let notificationName = UITextView.textDidChangeNotification
86 | #else
87 | let notificationName = NSNotification.Name.UITextView.textDidChangeNotification
88 | #endif
89 |
90 | NotificationCenter.default.addObserver(self,
91 | selector: #selector(textDidChange),
92 | name: notificationName,
93 | object: nil)
94 |
95 | placeholderLabel.font = font
96 | placeholderLabel.textColor = placeholderColor
97 | placeholderLabel.textAlignment = textAlignment
98 | placeholderLabel.text = placeholder
99 | placeholderLabel.numberOfLines = 0
100 | placeholderLabel.backgroundColor = UIColor.clear
101 | placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
102 | addSubview(placeholderLabel)
103 | updateConstraintsForPlaceholderLabel()
104 | }
105 |
106 | private func updateConstraintsForPlaceholderLabel() {
107 | var newConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(\(textContainerInset.left + textContainer.lineFragmentPadding))-[placeholder]",
108 | options: [],
109 | metrics: nil,
110 | views: ["placeholder": placeholderLabel])
111 | newConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-(\(textContainerInset.top))-[placeholder]",
112 | options: [],
113 | metrics: nil,
114 | views: ["placeholder": placeholderLabel])
115 | newConstraints.append(NSLayoutConstraint(
116 | item: placeholderLabel,
117 | attribute: .width,
118 | relatedBy: .equal,
119 | toItem: self,
120 | attribute: .width,
121 | multiplier: 1.0,
122 | constant: -(textContainerInset.left + textContainerInset.right + textContainer.lineFragmentPadding * 2.0)
123 | ))
124 | removeConstraints(placeholderLabelConstraints)
125 | addConstraints(newConstraints)
126 | placeholderLabelConstraints = newConstraints
127 | }
128 |
129 | @objc private func textDidChange() {
130 | placeholderLabel.isHidden = !text.isEmpty
131 | }
132 |
133 | open override func layoutSubviews() {
134 | super.layoutSubviews()
135 | placeholderLabel.preferredMaxLayoutWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2.0
136 | }
137 |
138 | deinit {
139 | #if swift(>=4.2)
140 | let notificationName = UITextView.textDidChangeNotification
141 | #else
142 | let notificationName = NSNotification.Name.UITextView.textDidChangeNotification
143 | #endif
144 |
145 | NotificationCenter.default.removeObserver(self,
146 | name: notificationName,
147 | object: nil)
148 | }
149 |
150 | }
151 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/VideoPlayerVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoPlayerVC.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/9.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import SuperPlayer
10 | import UIKit
11 |
12 | class VideoPlayerVC: ViewController {
13 | static let shared: VideoPlayerVC = {
14 | let vc = VideoPlayerVC()
15 | vc.modalPresentationStyle = .fullScreen
16 | return vc
17 | }()
18 |
19 | var urlStr = "" {
20 | didSet {
21 | isDownloaded = urlStr.contains(".mp4")
22 | }
23 | }
24 |
25 | let playerView = SuperPlayerView()
26 | let downloadBtn = UIButton()
27 | var isDownloaded = false {
28 | didSet {
29 | downloadBtn.isHidden = urlStr.starts(with: "http://127.0.0.1") || isDownloaded
30 | }
31 | }
32 |
33 | override func viewDidLoad() {
34 | super.viewDidLoad()
35 |
36 | // 设置代理,用于接受事件
37 | playerView.delegate = self
38 | // 设置父View,_playerView会被自动添加到下面
39 | playerView.fatherView = contentView
40 | view.backgroundColor = .black
41 | downloadBtn.setTitle("下载", for: [])
42 | downloadBtn.titleLabel?.font = .systemFont(ofSize: 14)
43 | downloadBtn.setTitleColor(.white, for: [])
44 |
45 | playerView.controlView.addSubview(downloadBtn)
46 | downloadBtn.snp.makeConstraints { make in
47 | make.right.top.equalTo(-safeAreaTopHeight)
48 | make.size.equalTo(CGSize(width: 80, height: 50))
49 | }
50 | downloadBtn.rx.tap.bind { [unowned self] in
51 | let defaultName = Date().string(withFormat: "yyyyMMddHHmmss")
52 | let alert = UIAlertController(title: "下载", message: "请输入下载名称", defaultActionButtonTitle: "取消", tintColor: nil)
53 | alert.addTextField { textfiled in
54 | textfiled.placeholder = "请输入下载名称"
55 | textfiled.text = defaultName
56 | textfiled.clearButtonMode = .always
57 | }
58 | alert.addAction(title: "确定", style: .default, isEnabled: true) { [unowned self] _ in
59 | DownLoadManage.shared.addDownload(fileName: alert.textFields?.first?.text ?? defaultName, path: self.playerView.playerModel.playingDefinitionUrl, autoStart: true)
60 | self.isDownloaded = true
61 | }
62 | self.present(alert, animated: true, completion: nil)
63 | }.disposed(by: rx.disposeBag)
64 | }
65 |
66 | class func show() {
67 | // let vc = NavigationController(rootViewController: shared)
68 | // vc.navigationBar.isHidden = true
69 | // currentViewController()?.present(vc, animated: true, completion: nil)
70 | currentViewController()?.navigationController?.pushViewController(shared)
71 | }
72 | }
73 |
74 | extension VideoPlayerVC: SuperPlayerDelegate {
75 | override func viewWillAppear(_ animated: Bool) {
76 | super.viewWillAppear(animated)
77 | let playerModel = SuperPlayerModel()
78 | // 设置播放地址,直播、点播都可以
79 | playerModel.videoURL = urlStr
80 | let playurl = SuperPlayerUrl()
81 | playurl.title = "原始"
82 | playurl.url = urlStr
83 | playerModel.multiVideoURLs = [playurl]
84 | // 开始播放
85 | playerView.play(with: playerModel)
86 | playerView.playerConfig.hwAcceleration = false
87 | navigationController?.navigationBar.isHidden = true
88 | (keyWindow.rootViewController as? UISplitViewController)?.presentsWithGesture = false
89 | }
90 |
91 | override func viewWillDisappear(_ animated: Bool) {
92 | super.viewWillDisappear(animated)
93 | navigationController?.navigationBar.isHidden = false
94 | (keyWindow.rootViewController as? UISplitViewController)?.presentsWithGesture = true
95 | playerView.resetPlayer()
96 | }
97 |
98 | func superPlayerBackAction(_ player: SuperPlayerView!) {
99 | // player.resetPlayer()
100 | // dismiss(animated: true, completion: nil)
101 | navigationController?.popViewController()
102 | }
103 |
104 | func superPlayerFullScreenChanged(_ player: SuperPlayerView!) {
105 | (player.controlView as? SPDefaultControlView)?.danmakuBtn.isHidden = true
106 |
107 | downloadBtn.snp.remakeConstraints { make in
108 | make.top.equalTo(0)
109 | make.right.equalTo(player.isFullScreen ? -100 : -15)
110 | make.size.equalTo(CGSize(width: 80, height: 50))
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View.swift
3 | //
4 | //
5 | // Created by 朱德坤 on 2019/4/16.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class View: UIView {
12 | override init(frame: CGRect) {
13 | super.init(frame: frame)
14 | makeUI()
15 | }
16 |
17 | required init?(coder aDecoder: NSCoder) {
18 | fatalError("init(coder:) has not been implemented")
19 | }
20 |
21 | func makeUI(){
22 | themeService.rx
23 | .bind({ $0.background }, to: rx.backgroundColor)
24 | .disposed(by: self.rx.disposeBag)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | //
4 | //
5 | // Created by 朱德坤 on 2019/3/6.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import DZNEmptyDataSet
10 | //import Hero
11 | import NVActivityIndicatorView
12 | import RxCocoa
13 | import RxSwift
14 | import SnapKit
15 | import UIKit
16 | import XLPagerTabStrip
17 | class ViewController: UIViewController, NVActivityIndicatorViewable {
18 | let isLoading = BehaviorRelay(value: false)
19 |
20 | var automaticallyAdjustsLeftBarButtonItem = true
21 | var canOpenFlex = true
22 | fileprivate var indicatorInfo = IndicatorInfo(title: "")
23 | var navigationTitle = "" {
24 | didSet {
25 | navigationItem.title = navigationTitle
26 | indicatorInfo.title = navigationTitle
27 | }
28 | }
29 |
30 | let spaceBarButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil)
31 |
32 | let emptyDataSetButtonTap = PublishSubject()
33 | var emptyDataSetTitle = ""
34 | var emptyDataSetDescription = "暂无数据"//"出错啦!\n没有你要访问的页面~\n\n"
35 | var emptyDataSetImage = R.image.pic_common_404()!
36 | var emptyDataSetImageTintColor = BehaviorRelay(value: nil)
37 |
38 | let motionShakeEvent = PublishSubject()
39 |
40 | lazy var contentView: UIView = {
41 | let view = UIView()
42 | self.view.addSubview(view)
43 | view.snp.makeConstraints { make in
44 | make.edges.equalTo(self.view.safeAreaLayoutGuide)
45 | }
46 | return view
47 | }()
48 |
49 | lazy var stackView: UIStackView = {
50 | let subviews: [UIView] = []
51 | let view = UIStackView(arrangedSubviews: subviews)
52 | view.axis = .vertical
53 | view.spacing = 0
54 | self.contentView.addSubview(view)
55 | view.snp.makeConstraints { make in
56 | make.edges.equalToSuperview()
57 | }
58 | return view
59 | }()
60 |
61 | lazy var backBarButton: UIBarButtonItem = {
62 | let view = UIBarButtonItem()
63 | view.title = ""
64 | return view
65 | }()
66 |
67 | lazy var closeBarButton: UIBarButtonItem = {
68 | let view = UIBarButtonItem(image: R.image.icon_navigation_back(),
69 | style: .plain,
70 | target: self,
71 | action: nil)
72 | return view
73 | }()
74 |
75 | public override func viewDidLoad() {
76 | super.viewDidLoad()
77 |
78 | // Do any additional setup after loading the view.
79 | makeUI()
80 | bindViewModel()
81 |
82 | closeBarButton.rx.tap.bind { [weak self] () in
83 | self?.dismiss(animated: true, completion: nil)
84 | }.disposed(by: rx.disposeBag)
85 | // Observe application did become active notification
86 | NotificationCenter.default
87 | .rx.notification(UIApplication.didBecomeActiveNotification)
88 | .subscribe { [weak self] _ in
89 | self?.didBecomeActive()
90 | }.disposed(by: rx.disposeBag)
91 | // Observe device orientation change
92 | NotificationCenter.default
93 | .rx.notification(UIDevice.orientationDidChangeNotification)
94 | .subscribe { [weak self] _ in
95 | self?.orientationChanged()
96 | }.disposed(by: rx.disposeBag)
97 | }
98 |
99 | public override func viewWillAppear(_ animated: Bool) {
100 | super.viewWillAppear(animated)
101 |
102 | if automaticallyAdjustsLeftBarButtonItem {
103 | adjustLeftBarButtonItem()
104 | }
105 | updateUI()
106 | }
107 |
108 | public override func viewDidAppear(_ animated: Bool) {
109 | super.viewDidAppear(animated)
110 | updateUI()
111 | logResourcesCount()
112 | }
113 |
114 | deinit {
115 | logDebug("\(type(of: self)): Deinited")
116 | logResourcesCount()
117 | }
118 |
119 | public override func didReceiveMemoryWarning() {
120 | super.didReceiveMemoryWarning()
121 | // Dispose of any resources that can be recreated.
122 | logDebug("\(type(of: self)): Received Memory Warning")
123 | }
124 |
125 | func makeUI() {
126 | // hero.isEnabled = true
127 | navigationItem.backBarButtonItem = backBarButton
128 | motionShakeEvent.subscribe(onNext: { () in
129 | //FIXME: - not complete
130 | // let theme = themeService.type.toggled()
131 | // themeService.switch(theme)
132 | }).disposed(by: rx.disposeBag)
133 | if #available(iOS 13.0, *) {
134 | if self.modalPresentationStyle == .pageSheet{
135 | self.modalPresentationStyle = .fullScreen
136 | }
137 | overrideUserInterfaceStyle = .light
138 | }
139 | themeService.rx
140 | .bind({ $0.background }, to: view.rx.backgroundColor)
141 | .bind({ $0.secondaryDark }, to: [backBarButton.rx.tintColor, closeBarButton.rx.tintColor])
142 | .disposed(by: rx.disposeBag)
143 |
144 | updateUI()
145 | }
146 |
147 | func bindViewModel() {
148 | emptyDataSetButtonTap.bind { [unowned self] in
149 | self.navigationController?.popToRootViewController(animated: true)
150 | self.dismiss(animated: true, completion: nil)
151 |
152 | }.disposed(by: rx.disposeBag)
153 | }
154 |
155 | func updateUI() {
156 | }
157 |
158 | override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
159 | if motion == .motionShake {
160 | motionShakeEvent.onNext(())
161 | }
162 | }
163 |
164 | func orientationChanged() {
165 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
166 | self.updateUI()
167 | }
168 | }
169 |
170 | func didBecomeActive() {
171 | updateUI()
172 | }
173 |
174 | // MARK: Adjusting Navigation Item
175 |
176 | func adjustLeftBarButtonItem() {
177 | if navigationController?.viewControllers.count ?? 0 > 1 { // Pushed
178 | navigationItem.leftBarButtonItem = nil
179 | } else if presentingViewController != nil { // presented
180 | navigationItem.leftBarButtonItem = closeBarButton
181 | }
182 | }
183 |
184 | @objc func closeAction(sender: AnyObject) {
185 | dismiss(animated: true, completion: nil)
186 | }
187 | }
188 |
189 | extension ViewController {
190 | func emptyView(withHeight height: CGFloat) -> UIView {
191 | let view = UIView()
192 | view.snp.makeConstraints { make in
193 | make.height.equalTo(height)
194 | }
195 | return view
196 | }
197 |
198 | public var emptyTableFooterView: UIView {
199 | return UIView(frame: CGRect(x: 0, y: 0, width: screenWidth, height: 0.01))
200 | }
201 |
202 | @objc func handleThreeFingerSwipe(swipeRecognizer: UISwipeGestureRecognizer) {
203 | if swipeRecognizer.state == .recognized {
204 | LibsManager.shared.showFlex()
205 |
206 | }
207 | }
208 | }
209 |
210 | extension Reactive where Base: ViewController {
211 | /// Bindable sink for `backgroundColor` property
212 | var emptyDataSetImageTintColorBinder: Binder {
213 | return Binder(base) { view, attr in
214 | view.emptyDataSetImageTintColor.accept(attr)
215 | }
216 | }
217 | }
218 |
219 | extension ViewController: DZNEmptyDataSetSource {
220 | func title(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! {
221 | return NSAttributedString(string: emptyDataSetTitle)
222 | }
223 |
224 | func description(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! {
225 | return NSAttributedString(string: emptyDataSetDescription)
226 | }
227 |
228 | func image(forEmptyDataSet scrollView: UIScrollView!) -> UIImage! {
229 | return emptyDataSetImage
230 | }
231 |
232 | func imageTintColor(forEmptyDataSet scrollView: UIScrollView!) -> UIColor! {
233 | return emptyDataSetImageTintColor.value
234 | }
235 |
236 | func backgroundColor(forEmptyDataSet scrollView: UIScrollView!) -> UIColor! {
237 | return .clear
238 | }
239 |
240 | func verticalOffset(forEmptyDataSet scrollView: UIScrollView!) -> CGFloat {
241 | return -60
242 | }
243 | }
244 |
245 | extension ViewController: DZNEmptyDataSetDelegate {
246 | func emptyDataSetShouldDisplay(_ scrollView: UIScrollView!) -> Bool {
247 | return !isLoading.value
248 | }
249 |
250 | func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView!) -> Bool {
251 | return true
252 | }
253 |
254 | func emptyDataSet(_ scrollView: UIScrollView!, didTap button: UIButton!) {
255 | emptyDataSetButtonTap.onNext(())
256 | }
257 | }
258 |
259 | extension ViewController: IndicatorInfoProvider {
260 |
261 | func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
262 | return self.indicatorInfo
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/ViewModelType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewModelType.swift
3 | // szwhExpressway
4 | //
5 | // Created by 朱德坤 on 2019/3/20.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxSwift
11 |
12 | protocol ViewModelType {
13 | associatedtype Input
14 | associatedtype Output
15 |
16 | func transform(input: Input) -> Output
17 | }
18 |
19 | class ViewModel: NSObject {
20 | var page = 1
21 |
22 | // let loading = ActivityIndicator()
23 | // let headerLoading = ActivityIndicator()
24 | // let footerLoading = ActivityIndicator()
25 | let loading = BehaviorRelay(value:false)
26 | let headerLoading = BehaviorRelay(value:false)
27 | let footerLoading = BehaviorRelay(value:false)
28 |
29 | /// 无更多数据 :false ,重置:true
30 | let noMoreDate = BehaviorRelay(value:false)
31 | override init() {
32 | super.init()
33 | }
34 |
35 | deinit {
36 | logDebug("\(type(of: self)): Deinited")
37 | logResourcesCount()
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Common/VipWebsites.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VipWebsites.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/9.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import HandyJSON
11 | struct VipAnalysis: HandyJSON, ListAble {
12 | var icon: UIImage?
13 | var text: String {
14 | return title
15 | }
16 |
17 | var id: String {
18 | return url
19 | }
20 |
21 | var selected = false
22 | var title = ""
23 | var url = ""
24 |
25 | static var vips: [VipAnalysis] = {
26 | let str = (try? String(contentsOf: R.file.vipwebsitesJson()!)) ?? ""
27 | return JSON(parseJSON: str).arrayValue.map(VipAnalysis.from(json:))
28 | }()
29 | }
30 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Download/DKM3u8Helper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DKM3u8Helper.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/11.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | class DKM3u8Helper {
11 | fileprivate var url = ""
12 | fileprivate var success: ((URL) -> Void)?
13 | fileprivate var failed: ((M3u8ParaseError) -> Void)?
14 | var m3u8Data: String = ""
15 | var tsSegmentArray = [M3u8TsSegmentModel]()
16 | var tsPlaylist = M3u8Playlist()
17 | var identifier = ""
18 | func parser(url: String, name: String, success: ((URL) -> Void)?, failed: ((M3u8ParaseError) -> Void)?) {
19 | self.url = url
20 | self.success = success
21 | self.failed = failed
22 | self.identifier = name
23 | do {
24 | try self.parse(url: url)
25 | } catch {
26 | if let aError = error as? M3u8ParaseError {
27 | DispatchQueue.main.async {
28 | failed?(aError)
29 | }
30 |
31 | } else {
32 | DispatchQueue.main.async {
33 | failed?(.Other)
34 | }
35 | }
36 | }
37 | }
38 |
39 | fileprivate func parse(url: String) throws {
40 | self.url = url
41 | guard let m3u8Url = URL(string: url) else { throw M3u8ParaseError.URLInvalid }
42 | guard let m3u8Content = try? String(contentsOf: m3u8Url) else {
43 | throw M3u8ParaseError.EmptyM3u8Content
44 | }
45 | // if !(url.hasPrefix("http://") || url.hasPrefix("https://")) {
46 | // throw M3u8ParaseError.URLInvalid
47 | // }
48 | if m3u8Content == "" {
49 | throw M3u8ParaseError.EmptyM3u8Content
50 | } else if m3u8Content.contains("EXT-X-STREAM-INF") {
51 | let arr = m3u8Content.split(separator: "\n").map { String($0) }
52 | if let preIndex = arr.firstIndex(where: { $0.contains("#EXT-X-STREAM-INF") }) {
53 | if let newUrl = arr[safe: preIndex + 1] {
54 | if !newUrl.hasPrefix("http") {
55 | try self.parse(url: self.getFullPath(url: url, newUrl: newUrl))
56 | }
57 | }
58 | } else {
59 | throw M3u8ParaseError.NoSteamInfo
60 | }
61 | } else {
62 | guard m3u8Content.range(of: "#EXTINF:") != nil else {
63 | throw M3u8ParaseError.NoEXTINFinfo
64 | }
65 |
66 | self.m3u8Data = m3u8Content
67 | if self.tsSegmentArray.count > 0 { self.tsSegmentArray.removeAll() }
68 |
69 | let segmentRange = m3u8Content.range(of: "#EXTINF:")!
70 | let segmentsString = String(m3u8Content.suffix(from: segmentRange.lowerBound)).components(separatedBy: "#EXT-X-ENDLIST")
71 | var segmentArray = segmentsString[0].components(separatedBy: "\n")
72 | segmentArray = segmentArray.filter { !$0.contains("#EXT-X-DISCONTINUITY") }
73 |
74 | while segmentArray.count > 2 {
75 | var segmentModel = M3u8TsSegmentModel()
76 |
77 | let segmentDurationPart = segmentArray[0].components(separatedBy: ":")[1]
78 | var segmentDuration: Float = 0.0
79 |
80 | if segmentDurationPart.contains(",") {
81 | segmentDuration = Float(segmentDurationPart.components(separatedBy: ",")[0])!
82 | } else {
83 | segmentDuration = Float(segmentDurationPart)!
84 | }
85 |
86 | var segmentURL = segmentArray[1]
87 | if !segmentURL.hasPrefix("http") {
88 | segmentURL = self.getFullPath(url: url, newUrl: segmentURL)
89 | }
90 | segmentModel.duration = segmentDuration
91 | segmentModel.locationURL = segmentURL
92 |
93 | self.tsSegmentArray.append(segmentModel)
94 |
95 | segmentArray.remove(at: 0)
96 | segmentArray.remove(at: 0)
97 | }
98 |
99 | self.tsPlaylist.tsSegmentArray = self.tsSegmentArray
100 | self.tsPlaylist.identifier = self.identifier
101 | let allts = self.tsSegmentArray.map { "#EXTINF:\($0.duration),\n\($0.locationURL)" }.joined(separator: "\n")
102 | self.writeToLocalM3U8file(allts: allts)
103 | }
104 | }
105 |
106 | func writeToLocalM3U8file(allts: String) {
107 | self.checkOrCreatedM3u8Directory()
108 |
109 | let filePath = getDocumentsDirectory().appendingPathComponent("m3u8Files").appendingPathComponent("\(self.tsPlaylist.identifier).m3u8")
110 |
111 | var header = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:15\n"
112 | header.append(allts)
113 | header.append("\n#EXT-X-ENDLIST\n")
114 |
115 | let writeData: Data = header.data(using: .utf8)!
116 | try! writeData.write(to: filePath)
117 | DispatchQueue.main.async { [weak self] in
118 | self?.success?(filePath)
119 | }
120 | }
121 |
122 | private func checkOrCreatedM3u8Directory() {
123 | let filePath = getDocumentsDirectory().appendingPathComponent("m3u8Files")
124 |
125 | if !FileManager.default.fileExists(atPath: filePath.path) {
126 | try! FileManager.default.createDirectory(at: filePath, withIntermediateDirectories: true, attributes: nil)
127 | }
128 | }
129 |
130 | internal func getFullPath(url: String, newUrl: String) -> String {
131 | var url = url
132 | url = url.replacingOccurrences(of: url.pathComponents.last!, with: "")
133 | return newUrl
134 | .split(separator: "/")
135 | .filter { !url.contains($0) }
136 | .reduce(url) { $0.appendingPathComponent(String($1)) }
137 | }
138 | }
139 |
140 | enum M3u8ParaseError: CustomStringConvertible, Error {
141 | /// 非法的url路径
142 | case URLInvalid
143 | /// m3u8文件内容获取失败
144 | case EmptyM3u8Content
145 | /// 没有获取到对应码率的视频
146 | case NoSteamInfo
147 | /// ts文件信息获取失败
148 | case NoEXTINFinfo
149 | case Other
150 |
151 | var description: String {
152 | switch self {
153 | case .URLInvalid: return "非法的url路径"
154 | case .EmptyM3u8Content: return "m3u8文件内容获取失败"
155 | case .NoSteamInfo: return "没有获取到对应码率的视频"
156 | case .NoEXTINFinfo: return "ts文件信息获取失败"
157 | case .Other: return "解析失败,未知错误"
158 | }
159 | }
160 | }
161 |
162 | struct M3u8TsSegmentModel {
163 | /// 起始时间
164 | var duration: Float = 0.0
165 | /// 原始地址
166 | var locationURL = ""
167 | /// 文件索引
168 | var index: Int = 0
169 | }
170 |
171 | class M3u8Playlist {
172 | var tsSegmentArray = [M3u8TsSegmentModel]()
173 | var length: Int { self.tsSegmentArray.count }
174 | var identifier = ""
175 | }
176 |
177 | public func getDocumentsDirectory() -> URL {
178 | let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
179 | let documentsDirectory = paths[0]
180 | return documentsDirectory
181 | }
182 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Download/DownLoadManage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DownLoadManage.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/16.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import RxRelay
10 | import UIKit
11 | class DownLoadManage {
12 | let downloads: BehaviorRelay<[M3U8Downloader]> = .init(value: [])
13 | static let shared = DownLoadManage()
14 |
15 | init() {
16 | let m3u8Dir = getDocumentsDirectory().appendingPathComponent("m3u8Files")
17 | if let enums = FileManager.default.enumerator(atPath: m3u8Dir.path)?.sorted(by: {
18 | ($0 as? String).unwrapped(or: "") > ($1 as? String).unwrapped(or: "")
19 | }) {
20 | enums.forEach { path in
21 | if let fileName = path as? String {
22 | if fileName.hasSuffix(".m3u8") {
23 | addDownload(fileName: fileName.replacingOccurrences(of: ".m3u8", with: ""),
24 | path: m3u8Dir.appendingPathComponent(fileName).absoluteString,
25 | autoStart: UserDefaults.autoStartDownload)
26 | }
27 | }
28 | }
29 | }
30 | }
31 |
32 | func addDownload(fileName: String, path: String, autoStart: Bool = false) {
33 | let newDownload = M3U8Downloader(fileName: fileName, m3u8URL: path)
34 | if (downloads.value.map { $0.directoryName }.contains(fileName)) {
35 | UIViewController.currentViewController()?.showAlert(title: "提示", message: "下载任务已存在", buttonTitles: ["继续下载", "重新下载"], highlightedButtonIndex: 0, completion: { [unowned self] index in
36 | if index == 1 {
37 | self.deleteDownload(fileName: fileName)
38 | newDownload.downloadStatus.filter { $0 == .started }.take(1).bind { [unowned self] _ in
39 | self.downloads.accept(self.downloads.value.filter { $0.directoryName != fileName } + [newDownload])
40 | }.disposed(by: newDownload.rx.disposeBag)
41 | newDownload.parse(autoStart: autoStart)
42 |
43 | } else {
44 | self.downloads.value.first(where: { $0.directoryName == fileName })?.parse()
45 | }
46 | })
47 | } else {
48 | downloads.accept(downloads.value.filter { $0.directoryName != fileName } + [newDownload])
49 | newDownload.parse(autoStart: autoStart)
50 | }
51 | }
52 |
53 | func deleteDownload(fileName: String, success: (() -> Void)? = nil) {
54 | let filePath = getDocumentsDirectory()
55 | .appendingPathComponent("m3u8Files")
56 | .appendingPathComponent(fileName + ".m3u8")
57 | .path
58 |
59 | if FileManager.default.fileExists(atPath: filePath) {
60 | try? FileManager.default.removeItem(atPath: filePath)
61 | }
62 |
63 | deleteDownloadContent(fileName: fileName, success: success)
64 | downloads.accept(downloads.value.filter { $0.fileName != fileName })
65 | }
66 |
67 | func deleteDownloadContent(fileName: String, success: (() -> Void)? = nil) {
68 | let filePath = getDocumentsDirectory()
69 | .appendingPathComponent("Downloads")
70 | .appendingPathComponent(fileName.replacingOccurrences(of: ".m3u8", with: ""))
71 | .path
72 | try? String(contentsOfFile: getDocumentsDirectory()
73 | .appendingPathComponent("m3u8Files")
74 | .appendingPathComponent(fileName + ".m3u8")
75 | .path)
76 | .split(separator: "\n")
77 | .filter { $0.contains("http") }
78 | .enumerated().reversed()
79 | .forEach { offset, element in
80 | appDelegate.sessionManagerBackground.remove(String(element), completely: true, onMainQueue: false) { _ in
81 | if offset == 0 {success?()}
82 | }
83 | }
84 | if FileManager.default.fileExists(atPath: filePath) {
85 | try? FileManager.default.removeItem(atPath: filePath)
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Download/DownloadViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DownloadViewController.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/6.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxSwift
11 | import SuperPlayer
12 | import UIKit
13 | class DownloadViewController: TableViewController {
14 | override func makeUI() {
15 | super.makeUI()
16 | navigationTitle = "下载"
17 | }
18 |
19 | override func bindViewModel() {
20 | tableView.footRefreshControl = nil
21 | tableView.headRefreshControl = nil
22 | tableView.register(cellWithClass: DownlaodCell.self)
23 | DownLoadManage.shared.downloads.asDriver().drive(tableView.rx.items(cellIdentifier: DownlaodCell.identifier, cellType: DownlaodCell.self)) { _, element, cell in
24 | cell.setup(info: element)
25 | }.disposed(by: rx.disposeBag)
26 |
27 | tableView.rx.modelSelected(M3U8Downloader.self).bind { [unowned self] info in
28 | self.loca(info: info)
29 | }.disposed(by: rx.disposeBag)
30 | }
31 |
32 | func loca(info: M3U8Downloader) {
33 | print(info.downloadStatus.value)
34 | }
35 |
36 | class DownlaodCell: TableViewCell {
37 | let stateBtn = UIButton(type: .custom)
38 | let playBtn = UIButton(type: .custom)
39 | let deleteBtn = UIButton(type: .custom)
40 | let shareBtn = UIButton(type: .custom)
41 | let progressLabel = UILabel(fontSize: 12, textColor: .textGray(), text: "已下载:1.00%")
42 | let nameLabel = UILabel(fontSize: 14, text: "文件1")
43 | override func makeUI() {
44 | super.makeUI()
45 | containerView.snp.makeConstraints { make in
46 | make.height.equalTo(65).priority(.high)
47 | }
48 | containerView.addSubviews([stateBtn, playBtn, deleteBtn, shareBtn, progressLabel, nameLabel])
49 | stateBtn.snp.makeConstraints { make in
50 | make.left.top.equalTo(10)
51 | make.width.height.equalTo(45)
52 | }
53 | nameLabel.snp.makeConstraints { make in
54 | make.top.equalTo(10)
55 | make.left.equalTo(stateBtn.snp.right).offset(15)
56 | make.right.equalToSuperview()
57 | }
58 | progressLabel.snp.makeConstraints { make in
59 | make.left.equalTo(nameLabel)
60 | make.top.equalTo(nameLabel.snp.bottom).offset(10)
61 | }
62 | playBtn.snp.makeConstraints { make in
63 | make.right.equalTo(-150)
64 | make.top.equalTo(nameLabel.snp.bottom).offset(3)
65 | make.size.equalTo(CGSize(width: 50, height: 30))
66 | }
67 | shareBtn.snp.makeConstraints { make in
68 | make.right.equalTo(-80)
69 | make.top.equalTo(nameLabel.snp.bottom).offset(3)
70 | make.size.equalTo(CGSize(width: 50, height: 30))
71 | }
72 | shareBtn.setTitle("分享", for: [])
73 | playBtn.setTitle("播放", for: [])
74 | deleteBtn.setTitle("删除", for: [])
75 | deleteBtn.snp.makeConstraints { make in
76 | make.right.equalTo(-10)
77 | make.top.equalTo(nameLabel.snp.bottom).offset(3)
78 | make.size.equalTo(CGSize(width: 50, height: 30))
79 | }
80 | playBtn.borderWidth = 0.5
81 | shareBtn.borderWidth = 0.5
82 | deleteBtn.borderWidth = 0.5
83 | deleteBtn.borderColor = .red
84 | deleteBtn.setTitleColor(.red, for: [])
85 | themeService.rx
86 | .bind({ $0.secondary }, to: [playBtn.rx.titleColor(for: []), playBtn.rx.borderColor,
87 | shareBtn.rx.titleColor(for: []), shareBtn.rx.borderColor])
88 | .bind({ $0.secondaryDark }, to: stateBtn.rx.titleColor(for: []))
89 | .bind({ $0.primary }, to: containerView.rx.backgroundColor)
90 | .disposed(by: rx.disposeBag)
91 | stateBtn.titleLabel?.font = .systemFont(ofSize: 9)
92 | }
93 |
94 | func setup(info: M3U8Downloader) {
95 | info.downloadStatus.map { $0.description }.bind(to: stateBtn.rx.title(for: [])).disposed(by: disposeBag)
96 | nameLabel.text = info.directoryName
97 | info.progress.map { "已下载\(Int($0 * 100))%" }.bind(to: progressLabel.rx.text).disposed(by: disposeBag)
98 | playBtn.rx.tap.bind { _ in
99 | if let playPath = info.getPlayPath() {
100 | VideoPlayerVC.shared.urlStr = playPath
101 | VideoPlayerVC.show()
102 | }
103 | }.disposed(by: disposeBag)
104 | stateBtn.rx.tap.bind { _ in
105 | if info.downloadStatus.value == .started {
106 | info.downloader.cancelDownloadSegment()
107 | } else {
108 | info.downloader.startDownload()
109 | }
110 | }.disposed(by: disposeBag)
111 | deleteBtn.rx.tap.bind { _ in
112 | UIViewController.currentViewController()?.showAlert(title: "确认删除", message: "将要删除下载内容", buttonTitles: ["重新下载", "删除内容", "取消"], highlightedButtonIndex: 0, completion: { index in
113 | if index == 0 {
114 | DownLoadManage.shared.deleteDownloadContent(fileName: info.fileName) {
115 | info.parse()
116 | }
117 | } else if index == 1 {
118 | DownLoadManage.shared.deleteDownload(fileName: info.fileName)
119 | }
120 | })
121 | }.disposed(by: disposeBag)
122 | shareBtn.rx.tap.bind { _ in
123 | var playPath = info.getPlayPath()
124 | UIViewController.currentViewController()?.showAlert(title: "重要提示", message: "已复制播放地址,您可以发送给局域网(同一WIFI下)的好友,她可以直接播放无需下载,如果您正在使用移动热点,分享及播放不会消耗流量。同一时间您只能分享一集(好友收看期间您可以收看同一集,播放其他视频,好友可能无法继续观看)", buttonTitles: ["确定"], highlightedButtonIndex: 0, completion: { _ in
125 | if let ip = VideoPlayServer.currentServer?.serverURL?.absoluteString {
126 | playPath = playPath?.replacingOccurrences(of: "http://127.0.0.1:8080/", with: ip)
127 | }
128 | UIPasteboard.general.string = playPath
129 | let url = getDocumentsDirectory()
130 | .appendingPathComponent("Downloads")
131 | .appendingPathComponent(info.fileName)
132 | .appendingPathComponent(info.fileName + ".m3u8")
133 | self.shareM3u8(fileUrl: url) // URL(string: playPath!)
134 |
135 | })
136 |
137 | }.disposed(by: disposeBag)
138 | }
139 |
140 | func shareM3u8(fileUrl: URL?) {
141 | guard let fileUrl = fileUrl else { return }
142 | let activityVC = UIActivityViewController(activityItems: [fileUrl], applicationActivities: nil)
143 | activityVC.popoverPresentationController?.sourceView = self.contentView
144 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
145 | UIViewController.currentViewController()!.present(activityVC, animated: true, completion: nil)
146 | }
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Download/M3U8Downloader.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 | import GCDWebServer
4 | import RxRelay
5 | open class M3U8Downloader: NSObject {
6 | public let downloader = VideoDownloader()
7 | public let progress: BehaviorRelay = .init(value: 0.0)
8 | public var fileName = ""
9 | var directoryName: String {
10 | self.fileName.replacingOccurrences(of: ".m3u8", with: "")
11 | }
12 |
13 | public var m3u8URL = ""
14 | private let m3u8Parser = DKM3u8Helper()
15 | public var downloadStatus: BehaviorRelay = .init(value: .paused)
16 |
17 | public init(fileName: String, m3u8URL: String) {
18 | self.fileName = fileName
19 | self.m3u8URL = m3u8URL
20 | super.init()
21 | self.downloader.downloadStatus.bind(to: self.downloadStatus).disposed(by: self.rx.disposeBag)
22 | self.downloader.progress.bind(to: self.progress).disposed(by: self.rx.disposeBag)
23 | }
24 |
25 | open func parse(autoStart: Bool = true) {
26 | DispatchQueue.global().async { [unowned self] in
27 | self.m3u8Parser.parser(url: self.m3u8URL, name: self.fileName, success: { [weak self] _ in
28 | guard let self = self else { return }
29 | self.downloader.tsPlaylist = self.m3u8Parser.tsPlaylist
30 | self.downloader.m3u8Data = self.m3u8Parser.m3u8Data
31 | if autoStart { self.downloader.startDownload() }
32 | }) { [weak self] error in
33 | print(error)
34 | showMessage(message: error.description)
35 | self?.downloadStatus.accept(.failed)
36 | }
37 | }
38 | }
39 |
40 | func getPlayPath() -> String? {
41 | return VideoPlayServer.getServer(name: self.directoryName)
42 | }
43 | }
44 |
45 | class VideoPlayServer {
46 | static var currentServer: GCDWebDAVServer?
47 | public static func getServer(name: String) -> String? {
48 | self.currentServer?.stop()
49 | let dirPath = getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(name).path
50 | self.currentServer = GCDWebDAVServer(uploadDirectory: dirPath)
51 | // self.currentServer?.start(withPort: 8080, bonjourName: nil)
52 | try? self.currentServer?.start(options: ["Port": 8080, "AutomaticallySuspendInBackground": false])
53 | let playPath = "http://127.0.0.1:8080/" + name + ".m3u8"
54 | return playPath
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Download/SegmentDownloader.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Tiercel
3 | protocol SegmentDownloaderDelegate {
4 | func segmentDownloadSucceeded(with downloader: SegmentDownloader)
5 | func segmentDownloadFailed(with downloader: SegmentDownloader)
6 | }
7 |
8 | class SegmentDownloader: NSObject {
9 | var fileName: String
10 | var filePath: String
11 | var downloadURL: String
12 | var duration: Float
13 | var index: Int
14 |
15 | lazy var downloadSession: SessionManager = {
16 | appDelegate.sessionManagerBackground
17 | }()
18 |
19 | var downloadTask: DownloadTask?
20 | var isDownloading = false
21 | var finishedDownload = false
22 |
23 | var delegate: SegmentDownloaderDelegate?
24 |
25 | init(with url: String, filePath: String, fileName: String, duration: Float, index: Int) {
26 | downloadURL = url
27 | self.filePath = filePath
28 | self.fileName = fileName
29 | self.duration = duration
30 | self.index = index
31 | }
32 |
33 | func startDownload() {
34 | if checkIfIsDownloaded() {
35 | finishedDownload = true
36 | delegate?.segmentDownloadSucceeded(with: self)
37 | } else {
38 | let url = downloadURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
39 | guard let taskURL = URL(string: url) else { return }
40 | isDownloading = true
41 | downloadTask = downloadSession.download(taskURL, headers: nil, fileName: fileName)
42 | downloadTask?.success { [weak self] task in
43 | guard task.status == .succeeded else{return}
44 | guard let self = self else { return }
45 | let destinationURL = self.generateFilePath()
46 |
47 | self.finishedDownload = true
48 | self.isDownloading = false
49 |
50 | if FileManager.default.fileExists(atPath: destinationURL.path) {
51 | return
52 | } else {
53 | do {
54 | let furl = URL(fileURLWithPath: task.filePath)
55 | try FileManager.default.moveItem(at: furl, to: destinationURL)
56 | self.delegate?.segmentDownloadSucceeded(with: self)
57 | } catch let error as NSError {
58 | print(error.localizedDescription)
59 | }
60 | }
61 | }.failure { [weak self] _ in
62 | guard let self = self else { return }
63 | self.finishedDownload = false
64 | self.isDownloading = false
65 | self.delegate?.segmentDownloadFailed(with: self)
66 | }
67 | }
68 | }
69 |
70 | func cancelDownload() {
71 | downloadSession.cancel(downloadURL)
72 | isDownloading = false
73 | }
74 |
75 | func pauseDownload() {
76 | downloadSession.suspend(downloadURL)
77 | isDownloading = false
78 | }
79 |
80 | func resumeDownload() {
81 | downloadSession.start(downloadURL)
82 | isDownloading = true
83 | }
84 |
85 | func checkIfIsDownloaded() -> Bool {
86 | let filePath = generateFilePath().path
87 |
88 | if FileManager.default.fileExists(atPath: filePath) {
89 | return true
90 | } else {
91 | return false
92 | }
93 | }
94 |
95 | func generateFilePath() -> URL {
96 | return getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(filePath).appendingPathComponent(fileName)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Download/VideoDownloader.swift:
--------------------------------------------------------------------------------
1 |
2 |
3 | import Foundation
4 | import RxRelay
5 | public enum Status: CustomStringConvertible {
6 | case started
7 | case paused
8 | case canceled
9 | case finished
10 | case failed
11 |
12 | public var description: String {
13 | switch self {
14 | case .started: return "下载中"
15 | case .paused: return "已暂停"
16 | case .canceled: return "已取消"
17 | case .finished: return "下载完成"
18 | case .failed: return "已停止"
19 | }
20 | }
21 | }
22 |
23 | open class VideoDownloader {
24 | public var downloadStatus: BehaviorRelay = .init(value: .paused)
25 | public var progress: BehaviorRelay = .init(value: 0)
26 |
27 | var m3u8Data: String = ""
28 | var tsPlaylist = M3u8Playlist()
29 | var segmentDownloaders = [SegmentDownloader]()
30 | var tsFilesIndex = 0
31 | var neededDownloadTsFilesCount = 0
32 | /// 所有TS任务的下载地址【包括下载、未下载的】
33 | var downloadURLs = [String]()
34 | var downloadingProgress: Float {
35 | let finishedDownloadFilesCount = segmentDownloaders.filter { $0.finishedDownload == true }.count
36 | let fraction = Float(finishedDownloadFilesCount) / Float(neededDownloadTsFilesCount)
37 | let roundedValue = round(fraction * 100) / 100
38 | return roundedValue
39 | }
40 |
41 | fileprivate var startDownloadIndex = 0
42 |
43 | open func startDownload() {
44 | checkOrCreatedM3u8Directory()
45 |
46 | var newSegmentArray = [M3u8TsSegmentModel]()
47 |
48 | let notInDownloadList = tsPlaylist.tsSegmentArray.filter { !downloadURLs.contains($0.locationURL) }
49 | neededDownloadTsFilesCount = tsPlaylist.length
50 | // 将m3u8中的ts转化成下载任务
51 | for i in 0 ..< notInDownloadList.count {
52 | let fileName = "\(tsFilesIndex).ts"
53 |
54 | let segmentDownloader = SegmentDownloader(with: notInDownloadList[i].locationURL,
55 | filePath: tsPlaylist.identifier,
56 | fileName: fileName,
57 | duration: notInDownloadList[i].duration,
58 | index: tsFilesIndex)
59 | segmentDownloader.delegate = self
60 |
61 | segmentDownloaders.append(segmentDownloader)
62 | downloadURLs.append(notInDownloadList[i].locationURL)
63 |
64 | var segmentModel = M3u8TsSegmentModel()
65 | segmentModel.duration = segmentDownloaders[i].duration
66 | segmentModel.locationURL = segmentDownloaders[i].fileName
67 | segmentModel.index = segmentDownloaders[i].index
68 | newSegmentArray.append(segmentModel)
69 |
70 | tsPlaylist.tsSegmentArray = newSegmentArray
71 |
72 | tsFilesIndex += 1
73 | }
74 | // 开启下载任务
75 | segmentDownloaders[0 ..< UserDefaults.maxDownloadTS].forEach { $0.startDownload() }
76 | // 更新任务状态
77 | downloadStatus.accept(.started)
78 | }
79 |
80 | func checkDownloadQueue() {}
81 |
82 | func updateLocalM3U8file() {
83 | checkOrCreatedM3u8Directory()
84 |
85 | let filePath = getDocumentsDirectory().appendingPathComponent("Downloads").appendingPathComponent(tsPlaylist.identifier).appendingPathComponent("\(tsPlaylist.identifier).m3u8")
86 |
87 | var header = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:15\n"
88 | var content = ""
89 |
90 | for i in 0 ..< tsPlaylist.tsSegmentArray.count {
91 | let segmentModel = tsPlaylist.tsSegmentArray[i]
92 |
93 | let length = "#EXTINF:\(segmentModel.duration),\n"
94 | let fileName = "http://127.0.0.1:8080/\(segmentModel.index).ts\n"
95 | content += (length + fileName)
96 | }
97 |
98 | header.append(content)
99 | header.append("#EXT-X-ENDLIST\n")
100 |
101 | let writeData: Data = header.data(using: .utf8)!
102 | if !FileManager.default.fileExists(atPath: filePath.path) {
103 | try! writeData.write(to: filePath)
104 | }
105 | }
106 |
107 | private func checkOrCreatedM3u8Directory() {
108 | let filePath = getDocumentsDirectory()
109 | .appendingPathComponent("Downloads")
110 | .appendingPathComponent(tsPlaylist.identifier)
111 |
112 | if !FileManager.default.fileExists(atPath: filePath.path) {
113 | try! FileManager.default.createDirectory(at: filePath, withIntermediateDirectories: true, attributes: nil)
114 | }
115 | }
116 |
117 | open func pauseDownloadSegment() {
118 | segmentDownloaders.forEach { $0.pauseDownload() }
119 | downloadStatus.accept(.paused)
120 | }
121 |
122 | open func cancelDownloadSegment() {
123 | segmentDownloaders.forEach { $0.cancelDownload() }
124 | downloadStatus.accept(.canceled)
125 | }
126 |
127 | open func resumeDownloadSegment() {
128 | segmentDownloaders.forEach { $0.resumeDownload() }
129 | downloadStatus.accept(.started)
130 | }
131 | }
132 |
133 | extension VideoDownloader: SegmentDownloaderDelegate {
134 | func segmentDownloadSucceeded(with downloader: SegmentDownloader) {
135 | let finishedDownloadFilesCount = segmentDownloaders.filter { $0.finishedDownload == true }.count
136 | progress.accept(downloadingProgress)
137 | updateLocalM3U8file()
138 |
139 | let downloadingFilesCount = segmentDownloaders.filter { $0.isDownloading == true }.count
140 |
141 | if finishedDownloadFilesCount == neededDownloadTsFilesCount {
142 | //全部下载完成
143 | downloadStatus.accept(.finished)
144 | } else if startDownloadIndex == neededDownloadTsFilesCount - 1 {
145 | //所有任务,只剩下载完成的,和下载中的
146 | } else if downloadingFilesCount < UserDefaults.maxDownloadTS || finishedDownloadFilesCount != neededDownloadTsFilesCount {
147 | //还有任务未开始下载
148 | if startDownloadIndex < neededDownloadTsFilesCount - 1 {
149 | startDownloadIndex += 1
150 | }
151 | segmentDownloaders[safe: startDownloadIndex]?.startDownload()
152 | }
153 | }
154 |
155 | func segmentDownloadFailed(with downloader: SegmentDownloader) {
156 | downloadStatus.accept(.failed)
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Home/HomeTabbarVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeTabbarVC.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/6.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class HomeTabbarVC: UITabBarController {
12 | let homeVC = NavigationController(rootViewController: HomeViewController())
13 | let downloadVC = NavigationController(rootViewController: DownloadViewController())
14 | let settingVC = NavigationController(rootViewController: SettingViewController())
15 | override func viewDidLoad() {
16 | super.viewDidLoad()
17 | viewControllers = [homeVC, downloadVC, settingVC]
18 | homeVC.tabBarItem.image = R.image.icon_navigation_web()
19 | homeVC.tabBarItem.title = "首页"
20 | downloadVC.tabBarItem.image = R.image.icon_cell_dir()
21 | downloadVC.tabBarItem.title = "下载"
22 | settingVC.tabBarItem.image = R.image.icon_tabbar_settings()
23 | settingVC.tabBarItem.title = "设置"
24 |
25 | // homeVC.tabBarItem
26 |
27 | themeService.rx
28 | .bind({ $0.textGray }, to: tabBar.rx.unselectedItemTintColor)
29 | .bind({ $0.secondary }, to: tabBar.rx.tintColor)
30 | .bind({ $0.primary }, to: tabBar.rx.barTintColor)
31 | .disposed(by: rx.disposeBag)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Home/HomeViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewController.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/6.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | class HomeViewController: ViewController {
11 | lazy var searchBar: UIView = {
12 | let searchBar = UIView()
13 | let textView = UITextField(placeholder: "请输入地址", placeholderSize: 14)
14 | searchBar.backgroundColor = UIColor.white
15 | searchBar.addSubview(textView)
16 | textView.snp.makeConstraints { make in
17 | make.edges.equalTo(UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8))
18 | }
19 | let imgV = UIImageView(image: R.image.icon_tabbar_search())
20 | textView.leftView = imgV
21 | textView.clearButtonMode = .whileEditing
22 | textView.leftViewMode = .always
23 | imgV.frame = CGRect(x: 10, y: 10, width: 34, height: 14)
24 | textView.font = UIFont.systemFont(ofSize: 14)
25 | imgV.contentMode = .center
26 | textView.rx.controlEvent(.editingDidEndOnExit).bind(onNext: { [unowned self] _ in
27 | if let url = URL(string: textView.text!) {
28 | currentWebVC.show(requestUrl: url)
29 | } else {
30 | showMessage(message: "请输入正确的地址")
31 | }
32 | }).disposed(by: rx.disposeBag)
33 | themeService.rx
34 | .bind({ $0.background }, to: textView.rx.backgroundColor)
35 | .bind({ $0.background }, to: searchBar.rx.backgroundColor)
36 | .bind({ $0.textGray }, to: textView.rx.placeholderColor)
37 | .disposed(by: self.rx.disposeBag)
38 | return searchBar
39 | }()
40 |
41 | lazy var collectionView: UICollectionView = {
42 | let layout = UICollectionViewFlowLayout()
43 | layout.scrollDirection = .vertical
44 | layout.sectionInset = .zero
45 | layout.minimumLineSpacing = 1
46 | layout.minimumInteritemSpacing = 1
47 | layout.itemSize = CGSize(width: screenWidth / 2 - 1, height: 120)
48 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
49 | collectionView.backgroundColor = .white
50 | collectionView.showsHorizontalScrollIndicator = false
51 | collectionView.register(cellWithClass: MenuItem.self)
52 | return collectionView
53 | }()
54 |
55 | override func makeUI() {
56 | super.makeUI()
57 | navigationTitle = "首页"
58 | contentView.addSubview(searchBar)
59 | searchBar.snp.makeConstraints { make in
60 | make.top.equalToSuperview()
61 | make.height.equalTo(50)
62 | make.left.right.equalToSuperview()
63 | }
64 | contentView.addSubview(collectionView)
65 | collectionView.snp.makeConstraints { make in
66 | make.edges.equalTo(UIEdgeInsets(top: 55, left: 0, bottom: 0, right: 0))
67 | }
68 | themeService
69 | .rx
70 | .bind({ $0.background }, to: collectionView.rx.backgroundColor)
71 | .disposed(by: rx.disposeBag)
72 | }
73 |
74 | override func bindViewModel() {
75 | let subject = PublishSubject<[SubMenu]>()
76 | let str = (try? String(contentsOf: R.file.platformJson()!)) ?? ""
77 | let arr = JSON(parseJSON: str).arrayValue
78 | let menus = arr.map(SubMenu.from(json:))
79 |
80 | subject
81 | .bind(to: collectionView.rx.items(cellIdentifier: String(describing: MenuItem.self), cellType: MenuItem.self)) { _, model, cell in
82 | cell.setup(data: model)
83 | }.disposed(by: rx.disposeBag)
84 | subject.onNext(menus)
85 | collectionView.rx.modelSelected(SubMenu.self).bind { item in
86 | if let url = item.url {
87 | currentWebVC.show(requestUrl: url)
88 | }
89 | // self.navigationController?.pushViewController(TestWebVC())
90 | }.disposed(by: rx.disposeBag)
91 | }
92 |
93 | override func viewWillAppear(_ animated: Bool) {
94 | super.viewWillAppear(animated)
95 | if waitToPresentVC != nil {
96 | present(waitToPresentVC!, animated: true, completion: nil)
97 | waitToPresentVC = nil
98 | }
99 | }
100 | }
101 |
102 | extension HomeViewController {
103 | struct SubMenu: HandyJSON {
104 | var url: URL?
105 | var image: URL?
106 | static func from(json: JSON) -> SubMenu {
107 | var menu = SubMenu()
108 | menu.url = json["url"].url
109 | menu.image = json["image"].url
110 | return menu
111 | }
112 | }
113 |
114 | /// 菜单cell
115 | class MenuItem: UICollectionViewCell {
116 | let titleLab = UILabel(fontSize: 16, textColor: .darkGray)
117 | let imageView = UIImageView(frame: .zero)
118 |
119 | override init(frame: CGRect) {
120 | super.init(frame: frame)
121 | contentView.addSubviews([titleLab, imageView])
122 | themeService.rx
123 | .bind({ $0.primaryDark }, to: rx.backgroundColor)
124 | .bind({ $0.text }, to: titleLab.rx.textColor)
125 | .disposed(by: rx.disposeBag)
126 | imageView.snp.makeConstraints { make in
127 | make.edges.equalTo(UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15))
128 | }
129 | imageView.contentMode = .scaleAspectFit
130 |
131 | titleLab.snp.makeConstraints {
132 | $0.center.equalToSuperview()
133 | }
134 | }
135 |
136 | func setup(data: SubMenu) {
137 | imageView.sd_setImage(with: data.image, completed: nil)
138 | //titleLab.text = String(data.url?.absoluteString.dropLast(5).dropFirst(11) ?? "")
139 | }
140 |
141 | required init?(coder aDecoder: NSCoder) {
142 | super.init(coder: aDecoder)
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Settings/SettingViewCells.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingViewCells.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/6.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import RxRelay
10 | import UIKit
11 | extension SettingViewController {
12 | class SwitchCell: BaseTableViewCell {
13 | let aSwitch = UISwitch()
14 | override func makeUI() {
15 | super.makeUI()
16 | leftImageView.contentMode = .center
17 | leftImageView.snp.remakeConstraints { make in
18 | make.size.equalTo(35)
19 | }
20 | rightImageView.contentMode = .center
21 | rightImageView.snp.remakeConstraints { make in
22 | make.size.equalTo(25)
23 | }
24 | stackView.insertArrangedSubview(aSwitch, at: 2)
25 |
26 | themeService.rx
27 | .bind({ $0.secondary }, to: [aSwitch.rx.tintColor, aSwitch.rx.onTintColor])
28 | .bind({ $0.secondary }, to: leftImageView.rx.tintColor)
29 | .bind({ $0.primary }, to: containerView.rx.backgroundColor)
30 | .disposed(by: rx.disposeBag)
31 | }
32 |
33 | override func bindViewModel(viewModel: TableCellViewModel) {
34 | super.bindViewModel(viewModel: viewModel)
35 | guard let viewModel = viewModel as? SettingSwitchCellViewModel else { return }
36 | viewModel.isEnabled.asDriver().drive(aSwitch.rx.isOn).disposed(by: disposeBag)
37 | aSwitch.rx.isOn.bind(to: viewModel.isEnabled).disposed(by: disposeBag)
38 | aSwitch.rx.isOn.bind(to: viewModel.switchChanged).disposed(by:disposeBag)
39 | }
40 | }
41 |
42 | class TextCell: BaseTableViewCell {
43 | override func makeUI() {
44 | super.makeUI()
45 | leftImageView.contentMode = .center
46 | leftImageView.snp.remakeConstraints { make in
47 | make.size.equalTo(35)
48 | }
49 | rightImageView.contentMode = .center
50 | rightImageView.snp.remakeConstraints { make in
51 | make.size.equalTo(25)
52 | }
53 | themeService.rx
54 | .bind({ $0.primary }, to: containerView.rx.backgroundColor)
55 | .disposed(by: rx.disposeBag)
56 | }
57 | }
58 | }
59 |
60 | class SettingCellViewModel: TableCellViewModel {
61 | init(with title: String, detail: String?, image: UIImage?, hidesDisclosure: Bool) {
62 | super.init()
63 | self.title.accept(title)
64 | self.detail.accept(detail)
65 | self.image.accept(image)
66 | self.hidesDisclosure.accept(hidesDisclosure)
67 | }
68 | }
69 |
70 | class SettingSwitchCellViewModel: SettingCellViewModel {
71 | let isEnabled = BehaviorRelay(value: false)
72 |
73 | let switchChanged: AnyObserver
74 | init(with title: String, detail: String?, image: UIImage?, isEnabled: Bool, valueChanged: @escaping (Bool) -> Void) {
75 | self.isEnabled.accept(isEnabled)
76 | self.switchChanged = .init(eventHandler: { event in
77 | switch event {
78 | case let .next(value):
79 | valueChanged(value)
80 | case .completed: break
81 | case .error: break
82 | }
83 | })
84 | super.init(with: title, detail: detail, image: image, hidesDisclosure: true)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Settings/SettingViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingViewController.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/6.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import RxDataSources
10 | import UIKit
11 | class SettingViewController: TableViewController {
12 | var viewModle = SettingViewModel()
13 |
14 | override func makeUI() {
15 | super.makeUI()
16 | navigationTitle = "设置"
17 | tableView.headRefreshControl = nil
18 | tableView.footRefreshControl = nil
19 | }
20 |
21 | override func bindViewModel() {
22 | super.bindViewModel()
23 | tableView.register(cellWithClass: TextCell.self)
24 | tableView.register(cellWithClass: SwitchCell.self)
25 | let output = viewModle.transform(input: .init())
26 |
27 | let datasource = RxTableViewSectionedReloadDataSource.init(configureCell: { (_, tableView, indexPath, item) -> UITableViewCell in
28 | switch item {
29 | case let .text(viewModel: cellModel):
30 | let cell = tableView.dequeueReusableCell(withClass: TextCell.self, for: indexPath)
31 | cell.bindViewModel(viewModel: cellModel)
32 | return cell
33 | case let .selects(viewModel: cellModel):
34 | let cell = tableView.dequeueReusableCell(withClass: SwitchCell.self, for: indexPath)
35 | cell.bindViewModel(viewModel: cellModel)
36 | return cell
37 | }
38 |
39 | }, titleForHeaderInSection: { (source, index) -> String? in
40 | source[index].title
41 | })
42 | output.items.drive(tableView.rx.items(dataSource: datasource)).disposed(by: rx.disposeBag)
43 |
44 | tableView.rx.modelSelected(SettingItem.self).bind { [unowned self] item in
45 | if item.viewModel.title.value == "主题设置" {
46 | let themes = ColorTheme.allValues.map { CommonListData(id: $0.rawValue.string, text: $0.title, selected: UserDefaults.standard.themeColor == $0.rawValue, icon: UIImage(color: $0.color, size: CGSize(width: 30, height: 30))) }
47 |
48 | showSelectVC(inVC: self, height: CGFloat(themes.count * 44), listDataProvider: { list in
49 | list = themes
50 | }) { list in
51 | let theme = ColorTheme(rawValue: Int(list.first!.id)!)!
52 | themeService.switch(ThemeType.currentTheme().withColor(color: theme))
53 | }
54 | } else if item.viewModel.title.value == "关于" {
55 | let webVC = WebViewController()
56 | webVC.requestURL = URL(string: "https://www.jianshu.com/p/f9d06ed27f24")
57 | self.navigationController?.pushViewController(webVC)
58 | } else if item.viewModel.title.value == "最大下载线程数" {
59 | self.changeMaxTs(vm: item.viewModel)
60 | }
61 | }.disposed(by: rx.disposeBag)
62 | tableView.rx.itemSelected.bind { [unowned self] indexPath in
63 | self.tableView.deselectRow(at: indexPath, animated: true)
64 | }.disposed(by: rx.disposeBag)
65 | }
66 |
67 | func changeMaxTs(vm: SettingCellViewModel) {
68 | let alert = UIAlertController(title: "最大下载线程数", message: nil, preferredStyle: .alert)
69 | alert.addTextField(text: UserDefaults.maxDownloadTS.string, placeholder: nil, editingChangedTarget: nil, editingChangedSelector: nil)
70 | alert.addAction(title: "确定", style: .default, isEnabled: true) { _ in
71 | UserDefaults.maxDownloadTS = alert.textFields?.first?.text?.int ?? 3
72 | vm.detail.accept(UserDefaults.maxDownloadTS.string)
73 | }
74 | alert.addAction(title: "取消", style: .cancel, isEnabled: true, handler: nil)
75 | alert.show()
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/DKVideo/Modules/Settings/SettingViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingViewModel.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/18.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxDataSources
11 |
12 | enum SettingSection: SectionModelType {
13 | case detail(title: String, items: [SettingItem])
14 |
15 | typealias Item = SettingItem
16 |
17 | var title: String {
18 | switch self {
19 | case .detail(let title, _): return title
20 | }
21 | }
22 |
23 | var items: [SettingItem] {
24 | switch self {
25 | case .detail(_, let items): return items.map { $0 }
26 | }
27 | }
28 |
29 | init(original: SettingSection, items: [Item]) {
30 | switch original {
31 | case .detail(let title, let items): self = .detail(title: title, items: items)
32 | }
33 | }
34 | }
35 |
36 | enum SettingItem {
37 | case text(viewModel: SettingCellViewModel)
38 | case selects(viewModel: SettingSwitchCellViewModel)
39 |
40 | var viewModel: SettingCellViewModel {
41 | switch self {
42 | case .text(let viewModel): return viewModel
43 | case .selects(let viewModel): return viewModel
44 | }
45 | }
46 | }
47 |
48 | class SettingViewModel: ViewModel {
49 | struct Input {}
50 |
51 | struct Output {
52 | let items: Driver<[SettingSection]>
53 | // ["夜间模式","主题设置","开启时自动下载","使用WKWebview","浏览桌面版网页","显示解析视图","清除缓存","移动网络下载","关于"]
54 | }
55 |
56 | func transform(input: Input) -> Output {
57 | let items: [SettingSection] = [
58 | .detail(title: "主题设置", items: [
59 | .text(viewModel: .init(with: "主题设置", detail: nil, image: R.image.icon_cell_theme()?.template, hidesDisclosure: false)),
60 | .selects(viewModel: .init(with: "夜间模式", detail: nil, image: R.image.icon_cell_night_mode()?.template, isEnabled: ThemeType.currentTheme().isDark, valueChanged: { isDark in
61 | if ThemeType.currentTheme().isDark != isDark {
62 | themeService.switch(ThemeType.currentTheme().toggled())
63 | }
64 | }))
65 | ]),
66 | .detail(title: "网页设置", items: [
67 | .selects(viewModel: .init(with: "使用WKWebview", detail: "性能好,兼容差", image: R.image.icon_navigation_web()?.template, isEnabled: UserDefaults.useWKWebview, valueChanged: { UserDefaults.useWKWebview = $0 })),
68 | .selects(viewModel: .init(with: "浏览桌面版网页", detail: "", image: R.image.icon_navigation_web()?.template, isEnabled: UserDefaults.isPCAgent, valueChanged: { UserDefaults.isPCAgent = $0 })),
69 | .selects(viewModel: .init(with: "显示解析视图", detail: "", image: R.image.icon_navigation_web()?.template, isEnabled: UserDefaults.showVipWebView, valueChanged: { UserDefaults.showVipWebView = $0 }))
70 | ]),
71 | .detail(title: "其他", items: [
72 | .text(viewModel: .init(with:
73 | "清除缓存", detail: "", image: R.image.icon_cell_dir()?.template, hidesDisclosure: false)),
74 | .text(viewModel: SettingCellViewModel(with: "最大下载线程数", detail: UserDefaults.maxDownloadTS.string, image: R.image.icon_navigation_refresh()?.template, hidesDisclosure: false)),
75 | .selects(viewModel: .init(with: "打开APP时自动下载", detail: nil, image: R.image.icon_navigation_refresh()?.template, isEnabled: UserDefaults.autoStartDownload, valueChanged: { UserDefaults.autoStartDownload = $0 })),
76 | .selects(viewModel: .init(with: "后台时下载", detail: "下次启动生效", image: R.image.icon_navigation_refresh()?.template, isEnabled: UserDefaults.backgroundDownload, valueChanged: { UserDefaults.backgroundDownload = $0 })),
77 | .selects(viewModel: .init(with: "使用流量下载", detail: "下次启动生效", image: R.image.icon_navigation_refresh()?.template, isEnabled: UserDefaults.downloadWithoutWifi, valueChanged: { UserDefaults.downloadWithoutWifi = $0 })),
78 | .text(viewModel: .init(with:
79 | "关于", detail: "", image: R.image.icon_cell_dir()?.template, hidesDisclosure: false))
80 | ])
81 | ]
82 | return Output(items: Driver.just(items))
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/DKVideo/Resources/AdStringFile.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdStringFile.swift
3 | // DKVideo
4 | //
5 | // Created by 朱德坤 on 2019/12/17.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | let adString = ["ajiajdiji"]
12 | var bccc = [
13 | // 优酷
14 | "atm.youku.com", "Fvid.atm.youku.com", "html.atm.youku.com", "valb.atm.youku.com", "valf.atm.youku.com", "valo.atm.youku.com", "valp.atm.youku.com", "lstat.youku.com", "speed.lstat.youku.com", "urchin.lstat.youku.com", "stat.youku.com", "static.lstat.youku.com", "valc.atm.youku.com", "vid.atm.youku.com", "walp.atm.youku.com",
15 | // 百度:
16 | // "a.baidu.com", "baidutv.baidu.com", "bar.baidu.com", "c.baidu.com", "cjhq.baidu.com", "cpro.baidu.com", "drmcmm.baidu.com", "e.baidu.com", "eiv.baidu.com", "hc.baidu.com", "hm.baidu.com", "ma.baidu.com", "nsclick.baidu.com", "spcode.baidu.com", "tk.baidu.com", "union.baidu.com", "ucstat.baidu.com", "utility.baidu.com", "utk.baidu.com", "focusbaiduafp.allyes.com",
17 |
18 | // 奇艺",
19 | // "afp.qiyi.com", "focusbaiduafp.allyes.com",
20 |
21 | // CNTV"
22 | "a.cctv.com", "a.cntv.cn", "ad.cctv.com", "d.cntv.cn", "adguanggao.eee114.com", "cctv.adsunion.com",
23 |
24 | // 新浪视频",
25 | "dcads.sina.com.cn",
26 |
27 | // pptv"
28 | "pp2.pptv.com",
29 |
30 | // 乐视"
31 | "pro.hoye.letv.com",
32 |
33 | // 搜狐高清"
34 | "images.sohu.com",
35 |
36 | // 我乐网"
37 | // "acs.56.com", "acs.agent.56.com", "acs.agent.v-56.com", "bill.agent.56.com", "bill.agent.v-56.com", "stat.56.com", "stat2.corp.56.com", "union.56.com", "uvimage.56.com", "v16.56.com",
38 |
39 | // 6间房"
40 | // "pole.6rooms.com", "shrek.6.cn", "simba.6.cn", "union.6.cn",
41 |
42 | // 土豆网"
43 | // "adextensioncontrol.tudou.com", "iwstat.tudou.com", "nstat.tudou.com", "stats.tudou.com", "*.p2v.tudou.com*", "at-img1.tdimg.com", "at-img2.tdimg.com", "at-img3.tdimg.com", "adplay.tudou.com", "adcontrol.tudou.com", "stat.tudou.com",
44 |
45 | // 酷6网"
46 | // "1.allyes.com.cn", "analytics.ku6.com", "gug.ku6cdn.com", "ku6.allyes.com", "ku6afp.allyes.com", "pq.stat.ku6.com", "st.vq.ku6.cn", "stat0.888.ku6.com", "stat1.888.ku6.com", "stat2.888.ku6.com", "stat3.888.ku6.com", "static.ku6.com", "v0.stat.ku6.com", "v1.stat.ku6.com", "v2.stat.ku6.com", "v3.stat.ku6.com",
47 |
48 | // 迅雷看看屏蔽:
49 | // "mcfg.sandai.net", "server1.adpolestar.net", "mpv.sandai.net", "mtips.xunlei.com", "biz5.sandai.net", "kkpgv.xunlei.com", "statis.kankan.xunlei.com", "float.sandai.net", "cl.kankan.xunlei.com", "advstat.xunlei.com", "pubstat.sandai.net", "msg.client.xunlei.com",
50 |
51 | // 腾讯视频屏蔽"
52 | "adslvfile.qq.com", "adsfile.qq.com", "play.qq.com", "adslvfile.qq.com", "tj.video.qq.com", "vv.video.qq.com", "livep.l.qq.com", "rcgi.video.qq.com", "aid.video.qq.com", "pinghot.qq.com", "54kfqq.com", "qqbm.zhuiying.net", "qqbmad.mycode8.net", "vhotlxp.video.qq.com", "livem.l.qq.com",
53 | "img.09mk.cn", "img.xiaohui2.cn", ".xiaohui", ".apple.com", "img2.", "sysapr.cn", "vlive.qqvideo", "image.kdmmm","baidu.com"
54 | ]
55 |
--------------------------------------------------------------------------------
/DKVideo/Resources/blank.caf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DKJone/DKVideo/75f858a2f9dc8ff7607bcdd9eab29481299d7bb5/DKVideo/Resources/blank.caf
--------------------------------------------------------------------------------
/DKVideo/Resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello World
5 |
6 |
34 |
35 |
36 |
37 |
38 |
39 |
246 |
247 |
248 |
249 |
250 |
--------------------------------------------------------------------------------
/HowToInterceptRequests.md:
--------------------------------------------------------------------------------
1 | 如何拦截并转发APP中的网络请求
2 | ===
3 | ####理论基础:URL Loading System
4 | ####主要实现类: [URLProtocol](https://developer.apple.com/documentation/foundation/nsurlprotocol)
5 |
6 | >iOS的Foundation框架提供了 URL Loading System 这个库(ULS),所有应用层的传输协议都可以通过ULS提供的基础类和协议来实现,你也可以你用它自定义自己通讯协议。
7 | >在每一个 HTTP 请求开始时,ULS创建一个合适的 `URLProtocol` 对象处理对应的 URL 请求,而我们需要做的就是写一个继承自 `URLProtocol` 的类,并通过` - registerClass: `方法注册我们的协议类,然后 URL 加载系统就会在请求发出时使用我们创建的协议对象对该请求进行处理。
8 |
9 | #`URLProtocol`
10 | 一个用于处理特定URL加载的抽象类,定义:
11 | ```swift
12 | class URLProtocol : NSObject
13 | ```
14 |
15 | 一个父类是`NSObject`的抽象类,并不是swift中的`Protocol`,
16 | 实现此抽象类后理论上可以拦截APP中所有的Cocoa层网络请求,
17 |
18 | 由此我们定义一个URLProtocol的子类,来拦截处理我们的网络请求,例如截取网络请求中的视频播放地址,为每个请求添加统一的请求头,过滤掉部分网络请求等。
19 |
20 | ```swift
21 | /// 网络请求拦截器
22 | class URLIntercept: URLProtocol { }
23 | ```
24 |
25 | 我们直接在`XCode`中点开`URLProtocol`
26 | 类的定义你主要有以下方法和属性
27 | ```swift
28 | /** 初始化方法 */
29 | public init(request: URLRequest,...)
30 | public init(task: URLSessionTask,...)
31 | /** 用于获取加载结果 */
32 | open var client: URLProtocolClient? { get }
33 |
34 | /** 当前加载的网络请求 */
35 | open var request: URLRequest { get }
36 |
37 | /** 参数是当前的网络请求,返回是否需要监控此请求 */
38 | open class func canInit(with request: URLRequest) -> Bool
39 | open class func canInit(with: URLSessionTask) -> Bool
40 |
41 | /** 根据当前的网络请求,返回一个我们自定义的网络请求 */
42 | open class func canonicalRequest(for request: URLRequest) -> URLRequest
43 |
44 | /** 调用此方法,应该开始加载网络请求 */
45 | open func startLoading()
46 | /** 实现方法以取消网络请求 */
47 | open func stopLoading()
48 |
49 | /** 获取网络请求中的关联属性 */
50 | open class func property(forKey key: String, in request: URLRequest) -> Any?
51 | /** 设置网络请求中的关联属性 */
52 | open class func setProperty(_ value: Any, forKey key: String, in request: NSMutableURLRequest)
53 | /** 移除网络请求中的关联属性 */
54 | open class func removeProperty(forKey key: String, in request: NSMutableURLRequest)
55 |
56 | /** 注册协议类*/
57 | open class func registerClass(_ protocolClass: AnyClass) -> Bool
58 | /** 取消注册 */
59 | open class func unregisterClass(_ protocolClass: AnyClass)
60 | }
61 | ```
62 | 从上面也不难看出我们需要重点实现 `canInit`来确定是否监控此条网络请求 ,实现`canonicalRequest`实现拦截的具体处理逻辑。
63 |
64 | 首先我们需要确定哪些球球需要处理,注意:在我们发起一个网络请求的时候,首先会调用`canInitWithRequest:`方法,询问是否对该请求进行处理,接着会调用`canonicalRequestForRequest:`来自定义一个`request`,新的请求(request)又会去调用`canInitWithRequest:`询问自定义的`request`是否需要处理,如果我们又返回`true`,然后又去调用了`canonicalRequestForRequest:`这样,就形成了一个死循环了,为了打破这种循环,我们给处理过的网络请求设置一个标识,再次检测到此标识就不在处理这条请求。
65 | ```swift
66 | let URLInterceptKey = "Intercepted"
67 | /// 返回是否监控此条网络请求
68 | /// - Parameter request: 网络请求
69 | override class func canInit(with request: URLRequest) -> Bool {
70 | print(request.url?.absoluteString ?? "")
71 | // 如果是已经拦截过的就放行,避免出现死循环
72 | if URLProtocol.property(forKey: URLInterceptKey, in: request) as? Bool ?? false {
73 | return false
74 | }
75 | // 不是网络请求,不处理
76 | if let urlScheme = request.url?.scheme?.lowercased() {
77 | if ["http", "https", "ftp"].contains(urlScheme) {
78 | return true
79 | }
80 | }
81 | // 不拦截其他
82 | return false
83 | }
84 |
85 | /// 设置我们自己的自定义请求
86 | /// - Parameter request: 当前的网络请求
87 | override class func canonicalRequest(for request: URLRequest) -> URLRequest {
88 | var mutableReqeust: URLRequest = request
89 | guard let urlStr = request.url?.absoluteString else { return request }
90 | // 广告拦截标识字符
91 | let adStrings = ["img.09mk.cn", "img.xiaohui2.cn", ".xiaohui", ".apple.com", "img2.", "sysapr.cn"]
92 | adStrings.forEach { str in
93 | if urlStr.contains(str) { mutableReqeust.url = nil }
94 | }
95 | // 视频播放拦截
96 | if urlStr.pathExtension.hasPrefix("m3u8") {
97 | print("=====video获取到视频路径===\n\(urlStr)")
98 | DispatchQueue.main.async {
99 | //调用视频播放器播放拦截到的视频地址
100 | }
101 | }
102 | return mutableReqeust
103 | }
104 |
105 | ```
106 | 然后我们需要实现发起和取消网络请求的方法,可以再此对拦截到的网络请求做统一的处理,如修改请求头信息,设置处理标识符等
107 | ```swift
108 | // 由于默认的task是只读属性,所以我们用newTask属性记录新发起的请求
109 | var newTask: URLSessionTask?
110 | override func startLoading() {
111 | // 给我们处理过的请求设置一个标识符, 防止无限循环,
112 | var request = self.request
113 | URLProtocol.setProperty(true, forKey: URLInterceptKey, in: request as! NSMutableURLRequest)
114 | let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
115 | //模拟PC版浏览器请求
116 | if UserDefaults.isPCAgent{
117 | request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36", forHTTPHeaderField:"User-Agent" )
118 | }else{
119 | request.setValue("Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", forHTTPHeaderField: "User-Agent")
120 | }
121 | self.newTask = session.dataTask(with: request)
122 | self.newTask?.resume()
123 | }
124 |
125 | override func stopLoading() {
126 | self.newTask?.cancel()
127 | }
128 |
129 | ```
130 | 我们创建`URLSession`时将代理设为了`self`,所以还要实现代理方法
131 | ```swift
132 | extension URLIntercept: URLSessionDelegate, URLSessionDataDelegate {
133 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
134 | //这里可以拦截返回的数据`data`
135 | client?.urlProtocol(self, didLoad: data)
136 | }
137 |
138 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
139 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
140 | completionHandler(.allow)
141 | }
142 |
143 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
144 | client?.urlProtocolDidFinishLoading(self)
145 | }
146 | }
147 | ```
148 |
149 | 到此我们已经完成了这个拦截类的实现,将其注册后就可以实现请求拦截了,我们直接在APPdelegate中进行注册
150 | ```swift
151 | class AppDelegate: UIResponder, UIApplicationDelegate {
152 | var window: UIWindow?
153 |
154 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
155 |
156 | URLProtocol.registerClass(URLIntercept.self)
157 | LibsManager.shared.configTheme()
158 | return true
159 | }
160 | }
161 | ```
162 | 运行程序后就可以拦截到网络请求了,但是到此还没有结束,我们在拦截`WKWebView`的请求时发现,只能拦截到第一条,之后的都不在被拦截处理。
163 | 主要是由于WKWebView为内部调用请求对应的scheme注册了对应的URLProtocol,所以我们的`URLProtocol`实现无法拦截到这些请求,于是我们需要把这些注册了的scheme `unregister`掉,具体可以参考[webkit-TestProtocol.mm](https://github.com/WebKit/webkit/blob/master/Tools/TestWebKitAPI/cocoa/TestProtocol.mm)中的单元测试代码(60-73行)。
164 | 其中调用的 `unregisterSchemeForCustomProtocol`为私有API
165 | 所以我们需要借助运行时,去动态调用,在swift5中,我们已经不再能够使用`performSelector`方法,我们也不准备使用OC去调用这些私有api,关于swift调用运行时方法将在另一篇文章中介绍这里直接写结果代码
166 | ```swift
167 | class AppDelegate: UIResponder, UIApplicationDelegate {
168 | var window: UIWindow?
169 |
170 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
171 | let browCont = WKWebView().value(forKey: "browsingContextController")didFinishLaunchingWithOptions
172 | //获取类,你也可以直接使用NSClassFromString("WKBrowsingContextController")获取
173 | let classType = type(of: browCont!) as! AnyClass
174 | //获取注册scheme方法
175 | if let method = extractMethodFrom(owner: classType, selector: NSSelectorFromString("registerSchemeForCustomProtocol:")) {
176 | //反向注册http和https两个scheme
177 | _ = method("http")
178 | _ = method("https")
179 | }
180 | URLProtocol.registerClass(URLIntercept.self)
181 | LibsManager.shared.configTheme()
182 |
183 | return true
184 | }
185 | }
186 |
187 | ```
188 | 反向注册scheme后,WKWebView中的http和https请求就会被我们实现的`URLIntercept`拦截到了
189 |
190 | 如果需要拦截其他网络框架的请求需要替换掉`URLSessionConfiguration`中的`protocolClasses`,在其中添加我们的拦截类,在APPdelegate中的`didFinishLaunchingWithOptions`中使用[Aspects](https://github.com/steipete/Aspects) 去Swizzle原获取方法(也可以使用MethodSwizzling)
191 | ```swift
192 | let rblock: @convention(block) (AspectInfo)-> Void = { info in
193 | let invocation = info.originalInvocation()
194 | var pros = [URLProtocol.Type]()
195 | invocation?.invoke()
196 | invocation?.getReturnValue(&pros)
197 | pros.append(URLIntercept.self)
198 | invocation?.setReturnValue(&pros)
199 | }
200 | try! type(of: URLSession.shared.configuration).aspect_hook(NSSelectorFromString("protocolClasses"), with: .positionInstead, usingBlock: rblock)
201 | ```
202 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 DKJone
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | source 'https://github.com/CocoaPods/Specs.git'
3 | platform :ios, '11.0'
4 |
5 | target 'DKVideo' do
6 | # Comment the next line if you don't want to use dynamic frameworks
7 | use_frameworks!
8 |
9 | # Pods for DKVideo
10 | # UI
11 | pod 'NVActivityIndicatorView'
12 | pod 'SuperPlayer'
13 | pod 'SnapKit'
14 | pod 'IQKeyboardManagerSwift'
15 | pod 'ChameleonFramework/Swift', :git => 'https://github.com/luckychris/Chameleon'
16 | pod 'Toast-Swift'
17 | pod 'XLPagerTabStrip'
18 | pod 'KafkaRefresh'
19 | pod 'DZNEmptyDataSet'
20 | pod 'Aspects'
21 | pod 'FloatingPanel'
22 | # Debug
23 | pod 'FLEX', :configurations => ['Debug']
24 |
25 | # Tools
26 | pod 'R.swift'
27 | pod 'SwifterSwift'
28 | pod "GCDWebServer/WebDAV"
29 | pod 'SwiftyJSON'
30 | pod 'AttributedLib'
31 | pod 'HandyJSON'
32 | pod 'Tiercel'
33 | # RX
34 | pod 'RxSwiftExt'
35 | pod 'NSObject+Rx'
36 | pod 'RxViewController'
37 | pod 'RxGesture'
38 | pod 'RxOptional'
39 | pod 'RxDataSources'
40 | pod 'RxTheme'
41 | pod 'RxSwift' , '~> 5.0.0'
42 |
43 | end
44 |
45 | # Cocoapods optimization, always clean project after pod updating
46 | post_install do |installer|
47 | Dir.glob(installer.sandbox.target_support_files_root + "Pods-*/*.sh").each do |script|
48 | flag_name = File.basename(script, ".sh") + "-Installation-Flag"
49 | folder = "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
50 | file = File.join(folder, flag_name)
51 | content = File.read(script)
52 | content.gsub!(/set -e/, "set -e\nKG_FILE=\"#{file}\"\nif [ -f \"$KG_FILE\" ]; then exit 0; fi\nmkdir -p \"#{folder}\"\ntouch \"$KG_FILE\"")
53 | File.write(script, content)
54 | end
55 |
56 | # enable tracing resources
57 | installer.pods_project.targets.each do |target|
58 | if target.name == 'RxSwift'
59 | target.build_configurations.each do |config|
60 | if config.name == 'Debug'
61 | config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['-D', 'TRACE_RESOURCES']
62 | end
63 | end
64 | end
65 |
66 | if target.name == "CocoaHTTPServer"
67 | target.build_configurations.each do |config|
68 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)', 'DD_LEGACY_MACROS=1']
69 | end
70 | end
71 | end
72 | end
73 |
74 |
75 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - AFNetworking (4.0.1):
3 | - AFNetworking/NSURLSession (= 4.0.1)
4 | - AFNetworking/Reachability (= 4.0.1)
5 | - AFNetworking/Security (= 4.0.1)
6 | - AFNetworking/Serialization (= 4.0.1)
7 | - AFNetworking/UIKit (= 4.0.1)
8 | - AFNetworking/NSURLSession (4.0.1):
9 | - AFNetworking/Reachability
10 | - AFNetworking/Security
11 | - AFNetworking/Serialization
12 | - AFNetworking/Reachability (4.0.1)
13 | - AFNetworking/Security (4.0.1)
14 | - AFNetworking/Serialization (4.0.1)
15 | - AFNetworking/UIKit (4.0.1):
16 | - AFNetworking/NSURLSession
17 | - Aspects (1.4.1)
18 | - AttributedLib (3.0.0)
19 | - ChameleonFramework/Default (2.1.0)
20 | - ChameleonFramework/Swift (2.1.0):
21 | - ChameleonFramework/Default
22 | - Differentiator (4.0.1)
23 | - DZNEmptyDataSet (1.8.1)
24 | - FLEX (4.1.1)
25 | - FloatingPanel (1.7.4)
26 | - GCDWebServer/Core (3.5.4)
27 | - GCDWebServer/WebDAV (3.5.4):
28 | - GCDWebServer/Core
29 | - HandyJSON (5.0.1)
30 | - IQKeyboardManagerSwift (6.5.5)
31 | - KafkaRefresh (1.4.8):
32 | - KafkaRefresh/Category (= 1.4.8)
33 | - KafkaRefresh/Configuration (= 1.4.8)
34 | - KafkaRefresh/Core (= 1.4.8)
35 | - KafkaRefresh/Default (= 1.4.8)
36 | - KafkaRefresh/Style (= 1.4.8)
37 | - KafkaRefresh/UIKit (= 1.4.8)
38 | - KafkaRefresh/Category (1.4.8)
39 | - KafkaRefresh/Configuration (1.4.8):
40 | - KafkaRefresh/UIKit/FootKit
41 | - KafkaRefresh/UIKit/HeadKit
42 | - KafkaRefresh/Core (1.4.8):
43 | - KafkaRefresh/Category
44 | - KafkaRefresh/Default (1.4.8):
45 | - KafkaRefresh/Style
46 | - KafkaRefresh/Style (1.4.8)
47 | - KafkaRefresh/UIKit (1.4.8):
48 | - KafkaRefresh/UIKit/FootKit (= 1.4.8)
49 | - KafkaRefresh/UIKit/HeadKit (= 1.4.8)
50 | - KafkaRefresh/UIKit/LayerKit (= 1.4.8)
51 | - KafkaRefresh/UIKit/FootKit (1.4.8):
52 | - KafkaRefresh/Category
53 | - KafkaRefresh/Core
54 | - KafkaRefresh/Default
55 | - KafkaRefresh/Style
56 | - KafkaRefresh/UIKit/LayerKit
57 | - KafkaRefresh/UIKit/HeadKit (1.4.8):
58 | - KafkaRefresh/Category
59 | - KafkaRefresh/Core
60 | - KafkaRefresh/Default
61 | - KafkaRefresh/Style
62 | - KafkaRefresh/UIKit/LayerKit
63 | - KafkaRefresh/UIKit/LayerKit (1.4.8):
64 | - KafkaRefresh/Category
65 | - KafkaRefresh/Default
66 | - Masonry (1.1.0)
67 | - MMLayout (0.3.0)
68 | - "NSObject+Rx (5.0.2)":
69 | - RxSwift (~> 5.0)
70 | - NVActivityIndicatorView (4.8.0):
71 | - NVActivityIndicatorView/Presenter (= 4.8.0)
72 | - NVActivityIndicatorView/Presenter (4.8.0)
73 | - R.swift (5.2.2):
74 | - R.swift.Library (~> 5.2.0)
75 | - R.swift.Library (5.2.0)
76 | - RxCocoa (5.1.1):
77 | - RxRelay (~> 5)
78 | - RxSwift (~> 5)
79 | - RxDataSources (4.0.1):
80 | - Differentiator (~> 4.0)
81 | - RxCocoa (~> 5.0)
82 | - RxSwift (~> 5.0)
83 | - RxGesture (3.0.1):
84 | - RxCocoa (~> 5.0)
85 | - RxSwift (~> 5.0)
86 | - RxOptional (4.1.0):
87 | - RxCocoa (~> 5)
88 | - RxSwift (~> 5)
89 | - RxRelay (5.1.1):
90 | - RxSwift (~> 5)
91 | - RxSwift (5.0.1)
92 | - RxSwiftExt (5.2.0):
93 | - RxSwiftExt/Core (= 5.2.0)
94 | - RxSwiftExt/RxCocoa (= 5.2.0)
95 | - RxSwiftExt/Core (5.2.0):
96 | - RxSwift (~> 5.0)
97 | - RxSwiftExt/RxCocoa (5.2.0):
98 | - RxCocoa (~> 5.0)
99 | - RxSwiftExt/Core
100 | - RxTheme (4.0.0):
101 | - RxCocoa (~> 5.0)
102 | - RxSwift (~> 5.0)
103 | - RxViewController (1.0.0):
104 | - RxCocoa (~> 5.0)
105 | - RxSwift (~> 5.0)
106 | - SDWebImage (5.7.3):
107 | - SDWebImage/Core (= 5.7.3)
108 | - SDWebImage/Core (5.7.3)
109 | - SnapKit (5.0.1)
110 | - SuperPlayer (3.2.5):
111 | - AFNetworking (~> 4.0)
112 | - Masonry (~> 1.1.0)
113 | - MMLayout (~> 0.3.0)
114 | - SDWebImage (~> 5.0)
115 | - SuperPlayer/Player (= 3.2.5)
116 | - SuperPlayer/Player (3.2.5):
117 | - AFNetworking (~> 4.0)
118 | - Masonry (~> 1.1.0)
119 | - MMLayout (~> 0.3.0)
120 | - SDWebImage (~> 5.0)
121 | - TXLiteAVSDK_Player (= 7.2.8932)
122 | - SwifterSwift (5.2.0):
123 | - SwifterSwift/AppKit (= 5.2.0)
124 | - SwifterSwift/CoreAnimation (= 5.2.0)
125 | - SwifterSwift/CoreGraphics (= 5.2.0)
126 | - SwifterSwift/CoreLocation (= 5.2.0)
127 | - SwifterSwift/Dispatch (= 5.2.0)
128 | - SwifterSwift/Foundation (= 5.2.0)
129 | - SwifterSwift/MapKit (= 5.2.0)
130 | - SwifterSwift/SceneKit (= 5.2.0)
131 | - SwifterSwift/SpriteKit (= 5.2.0)
132 | - SwifterSwift/StoreKit (= 5.2.0)
133 | - SwifterSwift/SwiftStdlib (= 5.2.0)
134 | - SwifterSwift/UIKit (= 5.2.0)
135 | - SwifterSwift/AppKit (5.2.0)
136 | - SwifterSwift/CoreAnimation (5.2.0)
137 | - SwifterSwift/CoreGraphics (5.2.0)
138 | - SwifterSwift/CoreLocation (5.2.0)
139 | - SwifterSwift/Dispatch (5.2.0)
140 | - SwifterSwift/Foundation (5.2.0)
141 | - SwifterSwift/MapKit (5.2.0)
142 | - SwifterSwift/SceneKit (5.2.0)
143 | - SwifterSwift/SpriteKit (5.2.0)
144 | - SwifterSwift/StoreKit (5.2.0)
145 | - SwifterSwift/SwiftStdlib (5.2.0)
146 | - SwifterSwift/UIKit (5.2.0)
147 | - SwiftyJSON (5.0.0)
148 | - Tiercel (3.2.0)
149 | - Toast-Swift (5.0.1)
150 | - TXLiteAVSDK_Player (7.2.8932)
151 | - XLPagerTabStrip (9.0.0)
152 |
153 | DEPENDENCIES:
154 | - Aspects
155 | - AttributedLib
156 | - ChameleonFramework/Swift (from `https://github.com/luckychris/Chameleon`)
157 | - DZNEmptyDataSet
158 | - FLEX
159 | - FloatingPanel
160 | - GCDWebServer/WebDAV
161 | - HandyJSON
162 | - IQKeyboardManagerSwift
163 | - KafkaRefresh
164 | - "NSObject+Rx"
165 | - NVActivityIndicatorView
166 | - R.swift
167 | - RxDataSources
168 | - RxGesture
169 | - RxOptional
170 | - RxSwift (~> 5.0.0)
171 | - RxSwiftExt
172 | - RxTheme
173 | - RxViewController
174 | - SnapKit
175 | - SuperPlayer
176 | - SwifterSwift
177 | - SwiftyJSON
178 | - Tiercel
179 | - Toast-Swift
180 | - XLPagerTabStrip
181 |
182 | SPEC REPOS:
183 | https://github.com/CocoaPods/Specs.git:
184 | - AFNetworking
185 | - Aspects
186 | - AttributedLib
187 | - Differentiator
188 | - DZNEmptyDataSet
189 | - FLEX
190 | - FloatingPanel
191 | - GCDWebServer
192 | - HandyJSON
193 | - IQKeyboardManagerSwift
194 | - KafkaRefresh
195 | - Masonry
196 | - MMLayout
197 | - "NSObject+Rx"
198 | - NVActivityIndicatorView
199 | - R.swift
200 | - R.swift.Library
201 | - RxCocoa
202 | - RxDataSources
203 | - RxGesture
204 | - RxOptional
205 | - RxRelay
206 | - RxSwift
207 | - RxSwiftExt
208 | - RxTheme
209 | - RxViewController
210 | - SDWebImage
211 | - SnapKit
212 | - SuperPlayer
213 | - SwifterSwift
214 | - SwiftyJSON
215 | - Tiercel
216 | - Toast-Swift
217 | - TXLiteAVSDK_Player
218 | - XLPagerTabStrip
219 |
220 | EXTERNAL SOURCES:
221 | ChameleonFramework:
222 | :git: https://github.com/luckychris/Chameleon
223 |
224 | CHECKOUT OPTIONS:
225 | ChameleonFramework:
226 | :commit: 347fcaa815f60281f9fbd32620c42d384dfc9b44
227 | :git: https://github.com/luckychris/Chameleon
228 |
229 | SPEC CHECKSUMS:
230 | AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce
231 | Aspects: 7595ba96a6727a58ebcbfc954497fc5d2fdde546
232 | AttributedLib: ad537d4f9285ed57fc4d9103dd83c2ee6be1582b
233 | ChameleonFramework: d21a3cc247abfe5e37609a283a8238b03575cf64
234 | Differentiator: 886080237d9f87f322641dedbc5be257061b0602
235 | DZNEmptyDataSet: 9525833b9e68ac21c30253e1d3d7076cc828eaa7
236 | FLEX: 81096107977a09836eb440173010ceeef01105d6
237 | FloatingPanel: 3c9d0e30fe350e1613157557769d2ec97f76b96b
238 | GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4
239 | HandyJSON: d45b8415c501e3affc59ca9762c1197ff0feb646
240 | IQKeyboardManagerSwift: 0fb93310284665245591f50f7a5e38de615960b7
241 | KafkaRefresh: aaae4ff23197f2ef38b6b22ca320740beac4bbb4
242 | Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
243 | MMLayout: d559edecd69fbcb348368ba90917e047ab1c544e
244 | "NSObject+Rx": 2eb2cf51fd1c554118449a7fbc36e4cd620b9e94
245 | NVActivityIndicatorView: d24b7ebcf80af5dcd994adb650e2b6c93379270f
246 | R.swift: 7c52cdc57a66840ffe6cbd8a823d732059d42a32
247 | R.swift.Library: 5ba4f1631300caf9a4d890186930da85d540769d
248 | RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601
249 | RxDataSources: efee07fa4de48477eca0a4611e6d11e2da9c1114
250 | RxGesture: a3f8dd6adf078110ed5e1c9c30d09d705a09852e
251 | RxOptional: b1fcd60856807a564c0215c2184b8d33e7826dc2
252 | RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9
253 | RxSwift: e2dc62b366a3adf6a0be44ba9f405efd4c94e0c4
254 | RxSwiftExt: 4ca80336f43c28f11a2825cdd2fc61dd6c044697
255 | RxTheme: 0b642f47c7d197a803c6b9c083964188c34f7ddb
256 | RxViewController: 7330a46e5c31cd680db169da4c9fc8676e975a81
257 | SDWebImage: 97351f6582ceca541ea294ba66a1fcb342a331c2
258 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
259 | SuperPlayer: 5fba5483acaf772d34a462fddfc92819a2d39410
260 | SwifterSwift: 334181863c416882d97b7a60c05054d9e4d799e2
261 | SwiftyJSON: 36413e04c44ee145039d332b4f4e2d3e8d6c4db7
262 | Tiercel: 37c00d3d309657951d00ae68dcb6ce8c7dccf584
263 | Toast-Swift: 9b6a70f28b3bf0b96c40d46c0c4b9d6639846711
264 | TXLiteAVSDK_Player: 7e2f5854f131f0de16ffdefa35081a36bb4f1e93
265 | XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5
266 |
267 | PODFILE CHECKSUM: 80a3aaf9544a1b06b4604f33c40323df34e08620
268 |
269 | COCOAPODS: 1.8.4
270 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | DKVideo - 一键VIP视频解析
2 | =====
3 | 一键解析各网站VIP视频,去广告播放
4 | 解析接口来自网络,本项目只截取解析完成的内容来播放。
5 | 项目地址:[https://github.com/DKJone/DKVideo](https://github.com/DKJone/DKVideo)
6 | ####1.安装
7 | #####1.1 开发者:
8 | 下载[源码](https://github.com/DKJone/DKVideo)
9 | 后pod update 更换bundleId,运行到手机/IPad上
10 | 或者下载以下ipa后重签名应用:[未签名版](https://ali-fir-pro-binary.fir.im/7e9430a85b84f8a2a69653a3215a91c244eb1001?auth_key=1577169072-0-0-03f51e437338bbaca8aaae81389b53c0)
11 | 注:推荐使用[ios-app-signer](https://github.com/DanTheMan827/ios-app-signer)签名,使用方法自行百度
12 | #####1.2 普通用户:
13 | 前往以下地址下载:[未签名](https://ali-fir-pro-binary.fir.im/7e9430a85b84f8a2a69653a3215a91c244eb1001?auth_key=1577169072-0-0-03f51e437338bbaca8aaae81389b53c0) 下载后使用 [Cydia Impactor](http://www.cydiaimpactor.com/)安装
14 |
15 | ####2.使用
16 | #####2.1 播放vip或超前点映视频
17 | 打开APP找到对应网站的视频然后点击右上角VIP按钮即可实现一键解析如下图
18 | 
19 | #####2.2 下载视频
20 | 在播放页右上角点击下载按钮,输入名称后即可下载
21 | *注意:如果下载提示出错,可以在下载中心点击最前面的下载状态以继续下载*
22 | 暂时只支持m3u8点播视频下载,直播流下载可能出错
23 | 
24 |
25 | #####2.3 切换VIP接口
26 | 长按右上角vip按钮2秒后松开即可切换(第1~3较稳定)
27 | 
28 |
29 | #####2.4 播放下载
30 | 在下载中心点击播放
31 | #####2.5 打开浏览器中的网页
32 | 可以复制地址到首页的搜索框粘贴后点确定即可或者点击浏览器分享按钮然后选择DKVIdeo
33 |
34 | #####2.6 更多设置
35 | 在设置中心自行设置,
36 | *部分网页无法打开可切换电脑版和手机版*
37 |
38 |
39 | ###本项目如何实现的:
40 | >项目基于Swift5.0,项目源码不适合新手阅读,如想尝试实现本项目,需要具备基础的IOS开发技能,熟悉Swift
41 | >项目中度使用RXSwift,
42 | >使用RXTheme实现主题定制及暗色适配,
43 | >使用SwiftyJSON和HandyJSON解析json数据
44 | >使用SnapKit布局UI
45 | >使用RSwift管理图片和文件
46 | >播放器使用腾讯的SuperPlayer(Ijkplayer和TXLiteAVSDK的封装 )
47 | >使用OBJCRuntime动态调用私有API(上架应用需要将方法名加密处理)
48 |
49 | 1.[如何拦截并转发APP中的网络请求](https://github.com/DKJone/DKVideo/blob/master/HowToInterceptRequests.md)
50 | 2.如何解析M3U8(hls)文件得到下载路径
51 | 3.将APP加入到系统分享
52 |
53 |
54 |
55 |
56 | ###TODO:
57 | * [x] 解析视频地址替换为腾讯的SuperPlayer播放
58 | * [x] 一键解析VIP视频
59 | * [x] 切换解析源
60 | * [x] 视频在线播放
61 | * [x] 获取PC版网页
62 | * [x] 从其他视频APP或网页直接跳转到DKVideo中播放和解析
63 | * [x] 视频下载
64 | * [x] 下载重命名
65 | * [x] 下载列表
66 | * [x] 删除下载
67 | * [x] 边下边播
68 | * [x] 暗色主题
69 | * [x] 自定义主题
70 | * [x] 局域网分享下载
71 | * [x] 后台下载
72 | * [ ] 缓存管理
73 | * [ ] 暴力解析获取所有可用的播放路径
74 |
75 | 更新内容:
76 | 2019-12-24
77 | 1.新增后台下载(替换原来下载为Tiercel)
78 | 2.新增局域网分享
79 | 3.新增使用流量时可选择关闭下载
80 | 4.修复综合解析重复弹出播放视图
81 | 5.修复iPAD播放页面快进手势与呼出侧边栏手势冲突
82 | 6.修复部分视频无法下载,修改错误提示
83 |
--------------------------------------------------------------------------------
/VideoShare/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | VideoShare
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleVersion
22 | 1
23 | NSExtension
24 |
25 | NSExtensionAttributes
26 |
27 | NSExtensionActivationRule
28 | TRUEPREDICATE
29 |
30 | NSExtensionPointIdentifier
31 | com.apple.share-services
32 | NSExtensionPrincipalClass
33 | VideoShare.ShareVC
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/VideoShare/ShareVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShareVC.swift
3 | // VideoShare
4 | //
5 | // Created by 朱德坤 on 2019/12/4.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | class ShareVC: UIViewController {
11 | let btn = UIButton(frame: CGRect(x: 0, y: 0, width: 300, height: 50))
12 | override func viewDidLoad() {
13 | super.viewDidLoad()
14 | view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.7)
15 | btn.center = view.center
16 | btn.setTitle("使用DKVideo打开", for: [])
17 | view.addSubview(btn)
18 | btn.addTarget(self, action: #selector(open), for: .touchUpInside)
19 |
20 | }
21 |
22 | @objc func open() {
23 | var urlStr = ""
24 | extensionContext?.inputItems.forEach { item in
25 | for provider in (item as? NSExtensionItem)?.attachments ?? [] {
26 | provider.loadItem(forTypeIdentifier: "public.url", options: nil, completionHandler: { [unowned self] url, _ in
27 | let str = (url as? URL)?.absoluteString ?? ""
28 | if !str.isEmpty {
29 | print(str)
30 | urlStr = str
31 | self.openMainAPP(url: str)
32 | self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
33 | return
34 | }
35 | })
36 | }
37 | }
38 | // if urlStr.isEmpty {
39 | // btn.setTitle("没有获取到视频链接", for: [])
40 | // }
41 | }
42 |
43 | func openMainAPP(url: String) {
44 | if let method = extractMethodFrom(owner: try! self.sharedApplication(), selector: NSSelectorFromString("openURL:")) {
45 | _ = method(URL(string: "DKVideo://\(url)"))
46 | }
47 | }
48 |
49 | func sharedApplication() throws -> UIApplication {
50 | var responder: UIResponder? = self
51 | while responder != nil {
52 | if let application = responder as? UIApplication {
53 | return application
54 | }
55 | responder = responder?.next
56 | }
57 |
58 | throw NSError(domain: "sharedApplication not found", code: 1, userInfo: nil)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/VideoShare/ShareViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShareViewController.swift
3 | // VideoShare
4 | //
5 | // Created by 朱德坤 on 2019/12/4.
6 | // Copyright © 2019 DKJone. All rights reserved.
7 | //
8 |
9 | import Social
10 | import UIKit
11 |
12 | class ShareViewController: SLComposeServiceViewController {
13 | override func isContentValid() -> Bool {
14 | // Do validation of contentText and/or NSExtensionContext attachments here
15 | return false
16 | }
17 |
18 | override func didSelectPost() {
19 | // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
20 |
21 | // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
22 | self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
23 | }
24 |
25 | override func configurationItems() -> [Any]! {
26 | // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
27 | let item = SLComposeSheetConfigurationItem()
28 | item?.title = "打开"
29 | return [item]
30 | }
31 |
32 | override func viewDidLoad() {
33 | self.view.backgroundColor = .red
34 | // extensionContext?.inputItems.forEach { item in
35 | // print(item)
36 | // let provider = (extensionContext?.inputItems.first as? NSExtensionItem)?.attachments?.first
37 | // provider?.loadItem(forTypeIdentifier: "public.url", options: nil, completionHandler: { url, _ in
38 | // let str = (url as? URL)?.absoluteString ?? ""
39 | // print(str)
40 | // if let method = extractMethodFrom(owner: try! self.sharedApplication(), selector: NSSelectorFromString("openURL:")) {
41 | // _ = method(URL(string: "DKVideo://\(str)"))
42 | // }
43 | // })
44 | // }
45 | }
46 |
47 | func sharedApplication() throws -> UIApplication {
48 | var responder: UIResponder? = self
49 | while responder != nil {
50 | if let application = responder as? UIApplication {
51 | return application
52 | }
53 |
54 | responder = responder?.next
55 | }
56 |
57 | throw NSError(domain: "UIInputViewController+sharedApplication.swift", code: 1, userInfo: nil)
58 | }
59 | }
60 |
61 | /// 动态获取方法
62 | /// - Parameters:
63 | /// - owner: 对象或者类对象
64 | /// - selector: 方法选择子
65 | func extractMethodFrom(owner: AnyObject, selector: Selector) -> ((Any?) -> Any)? {
66 | let method: Method?
67 | if owner is AnyClass {
68 | method = class_getClassMethod(owner as? AnyClass, selector)
69 | } else {
70 | print(type(of: owner))
71 | method = class_getInstanceMethod(type(of: owner), selector)
72 | }
73 |
74 | if let one = method {
75 | let implementation = method_getImplementation(one)
76 |
77 | typealias Function = @convention(c) (AnyObject, Selector, Any?) -> Void
78 |
79 | let function = unsafeBitCast(implementation, to: Function.self)
80 |
81 | return { userinfo in function(owner, selector, userinfo) }
82 |
83 | } else {
84 | return nil
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/VideoShare/VideoShare.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------