8 changed files with 591 additions and 6 deletions
-
24kplayer.xcodeproj/project.pbxproj
-
2kplayer/core/MediaItem.swift
-
60kplayer/detail/BrowserController.swift
-
22kplayer/detail/DetailViewController+Show.swift
-
4kplayer/video/SVideoModel.swift
-
266kplayer/web/KBrowserView.swift
-
189kplayer/web/WebView.swift
-
26kplayer/web/WebViewModel.swift
@ -0,0 +1,266 @@ |
|||||
|
// |
||||
|
// KBrowserView.swift |
||||
|
// SwiftUIWebView |
||||
|
// |
||||
|
// Created by Md. Yamin on 4/24/20. |
||||
|
// Copyright © 2020 Md. Yamin. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
import SwiftUI |
||||
|
|
||||
|
struct KBrowserView: View { |
||||
|
@ObservedObject var viewModel = WebViewModel() |
||||
|
@State var downloadOptions = false |
||||
|
@State var showLoader = false |
||||
|
@State var showVideoView = false |
||||
|
@State var message = "" |
||||
|
@State var webTitle = "" |
||||
|
@State var dlUrls = [String]() |
||||
|
|
||||
|
var item: String |
||||
|
var completionHandler: (() -> Void)? |
||||
|
|
||||
|
@State var videoModel: SVideoModel? |
||||
|
|
||||
|
// For WebView's forward and backward navigation |
||||
|
var webViewNavigationBar: some View { |
||||
|
VStack(spacing: 0) { |
||||
|
Divider() |
||||
|
HStack { |
||||
|
Spacer() |
||||
|
Button(action: { |
||||
|
self.viewModel.webViewNavigationPublisher.send(.backward) |
||||
|
}) { |
||||
|
Image(systemName: "chevron.left") |
||||
|
.font(.system(size: 20, weight: .regular)) |
||||
|
.imageScale(.large) |
||||
|
.foregroundColor(.gray) |
||||
|
} |
||||
|
Group { |
||||
|
Spacer() |
||||
|
Divider() |
||||
|
Spacer() |
||||
|
} |
||||
|
Button(action: { |
||||
|
self.viewModel.webViewNavigationPublisher.send(.forward) |
||||
|
}) { |
||||
|
Image(systemName: "chevron.right") |
||||
|
.font(.system(size: 20, weight: .regular)) |
||||
|
.imageScale(.large) |
||||
|
.foregroundColor(.gray) |
||||
|
} |
||||
|
Group { |
||||
|
Spacer() |
||||
|
Divider() |
||||
|
Spacer() |
||||
|
} |
||||
|
Button(action: { |
||||
|
self.viewModel.webViewNavigationPublisher.send(.reload) |
||||
|
}) { |
||||
|
Image(systemName: "arrow.clockwise") |
||||
|
.font(.system(size: 20, weight: .regular)) |
||||
|
.imageScale(.large) |
||||
|
.foregroundColor(.gray).padding(.bottom, 4) |
||||
|
} |
||||
|
Spacer() |
||||
|
}.frame(height: 45) |
||||
|
Divider() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var body: some View { |
||||
|
ZStack { |
||||
|
VStack(spacing: 0) { |
||||
|
HStack { |
||||
|
Button(action: { |
||||
|
completionHandler!() |
||||
|
}, label: { |
||||
|
Text("cancel") |
||||
|
}) |
||||
|
.buttonStyle(BorderlessButtonStyle()) |
||||
|
Spacer() |
||||
|
Button(action: { |
||||
|
self.viewModel.valuePublisher.send("") |
||||
|
}, label: { |
||||
|
Text("download") |
||||
|
}) |
||||
|
.buttonStyle(BorderlessButtonStyle()).confirmationDialog("Open", isPresented: $downloadOptions) { |
||||
|
let dlcount = dlUrls.count |
||||
|
|
||||
|
ForEach(0..<dlcount) { index in |
||||
|
let name = makeLabel(url: dlUrls[index]) |
||||
|
|
||||
|
Button(name) { |
||||
|
self.preview(url: dlUrls[index]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Button("cancel", role: .cancel) { |
||||
|
downloadOptions = false |
||||
|
} |
||||
|
}.onReceive(self.viewModel.downloadPublisher.receive(on: RunLoop.main)) { value in |
||||
|
print(value) |
||||
|
downloadOptions = true |
||||
|
dlUrls = value |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
/* Here I created a text field that takes string value and when send |
||||
|
button is clicked 'viewModel.valuePublisher' sends that value to WebView |
||||
|
then WebView sends that value to web app that you will load. In this |
||||
|
project's local .html file can not receive it because it is static you should |
||||
|
test with a web app then it will work because static website can not receive values |
||||
|
at runtime where dynamic web app can */ |
||||
|
|
||||
|
Text(webTitle).font(.title).onReceive(self.viewModel.showWebTitle.receive(on: RunLoop.main)) { value in |
||||
|
self.webTitle = value |
||||
|
} |
||||
|
|
||||
|
/* This is our WebView. Here if you pass .localUrl it will load LocalWebsite.html file |
||||
|
into the WebView and if you pass .publicUrl it will load the public website depending on |
||||
|
your url provided. See WebView implementation for more info. */ |
||||
|
WebView(viewModel: viewModel, url: item).overlay ( |
||||
|
RoundedRectangle(cornerRadius: 4, style: .circular) |
||||
|
.stroke(Color.gray, lineWidth: 0.5) |
||||
|
).padding(.leading, 20).padding(.trailing, 20) |
||||
|
|
||||
|
webViewNavigationBar |
||||
|
}.onReceive(self.viewModel.showLoader.receive(on: RunLoop.main)) { value in |
||||
|
self.showLoader = value |
||||
|
} |
||||
|
|
||||
|
// A simple loader that is shown when WebView is loading any page and hides when loading is finished. |
||||
|
if showLoader { |
||||
|
// Loader() |
||||
|
} |
||||
|
}.fullScreenCover(isPresented: $showVideoView) { |
||||
|
SVideoPlayer(completionHandler: { saved in |
||||
|
showVideoView = false |
||||
|
}, model: videoModel!) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func makeLabel(url s: String) -> String { |
||||
|
var name = s |
||||
|
if let u = URL(string: s) { |
||||
|
name = u.lastPathComponent |
||||
|
if s.contains("720") { |
||||
|
name = name + "720 " |
||||
|
} |
||||
|
if s.contains("1080") { |
||||
|
name = name + "1080 " |
||||
|
} |
||||
|
if s.contains("480") { |
||||
|
name = name + "480 " |
||||
|
} |
||||
|
} |
||||
|
return name |
||||
|
} |
||||
|
|
||||
|
func preview(url: String) { |
||||
|
let url2 = URL(string: url)! |
||||
|
|
||||
|
if (url2.pathExtension == "zip") { |
||||
|
downloadZip(url2, path: "download") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (url2.pathExtension == "jpg") { |
||||
|
downloadZip(url2, path: "images/new") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let name = url2.lastPathComponent |
||||
|
let host = item |
||||
|
let hostcomp = host.split(separator: ".") |
||||
|
let site = String(hostcomp[hostcomp.count-2]) |
||||
|
|
||||
|
// if ((url2.pathExtension == "mp4" || url2.pathExtension == "m3u8") && dl) { |
||||
|
// NetworkManager.sharedInstance.downloadToServer(path: site, url: url2, result: { |
||||
|
// (r) in |
||||
|
// print(r) |
||||
|
// self.showAlert(title: "download ready", message: r) |
||||
|
// if (r == "exists") { |
||||
|
// |
||||
|
// } |
||||
|
// }) |
||||
|
// return |
||||
|
// } |
||||
|
|
||||
|
let item = MediaItem(name: name, path: name, root: site, type: ItemType.VIDEO) |
||||
|
|
||||
|
if url.starts(with: "/") { |
||||
|
item.externalURL = host + url |
||||
|
} |
||||
|
else { |
||||
|
item.externalURL = url |
||||
|
} |
||||
|
|
||||
|
if hostcomp.count > 2 { |
||||
|
let hostEnd = String(hostcomp[1] + "." + hostcomp[2]) |
||||
|
|
||||
|
getCookieHeader(for: hostEnd) { dictionary in |
||||
|
print(dictionary) |
||||
|
|
||||
|
item.cookies = dictionary |
||||
|
self.showVideo(selectedItem: item) |
||||
|
} |
||||
|
} |
||||
|
else { |
||||
|
showVideo(selectedItem: item) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func getCookieHeader(for domain: String? = nil, completion: @escaping (String)->()) { |
||||
|
let httpCookieStore = viewModel.httpCookieStore! |
||||
|
var cookieDict = [HTTPCookie]() |
||||
|
httpCookieStore.getAllCookies { cookies in |
||||
|
for cookie in cookies { |
||||
|
if let domain = domain { |
||||
|
if cookie.domain.contains(domain) { |
||||
|
cookieDict.append(cookie) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
let values = HTTPCookie.requestHeaderFields(with: cookieDict) |
||||
|
|
||||
|
completion(values["Cookie"]!) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func showVideo(selectedItem: MediaItem) { |
||||
|
var se = selectedItem |
||||
|
var children = [MediaItem]() |
||||
|
var clonedChildren = [MediaItem]() |
||||
|
var baseItem = selectedItem |
||||
|
|
||||
|
if baseItem.type == ItemType.SNAPSHOT { |
||||
|
baseItem = selectedItem.parent! |
||||
|
} |
||||
|
|
||||
|
children = baseItem.children |
||||
|
clonedChildren = baseItem.clone().children |
||||
|
|
||||
|
videoModel = SVideoModel(allItems: children, currentSnapshot: se, baseItem: baseItem) |
||||
|
|
||||
|
videoModel!.edit = LocalManager.sharedInstance.settings.edit |
||||
|
videoModel!.loop = LocalManager.sharedInstance.settings.autoloop |
||||
|
videoModel!.zoomed = LocalManager.sharedInstance.settings.zoomed |
||||
|
|
||||
|
showVideoView = true |
||||
|
} |
||||
|
|
||||
|
private func downloadZip(_ url: URL, path: String) { |
||||
|
NetworkManager.sharedInstance.download(url: url, path: path) { url in |
||||
|
// self.showAlert(title: url.lastPathComponent, message: "ready") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
struct ContentView_Previews: PreviewProvider { |
||||
|
static var previews: some View { |
||||
|
KBrowserView(item: "www.google.com") |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,189 @@ |
|||||
|
// |
||||
|
// WebView.swift |
||||
|
// SwiftUIWebView |
||||
|
// |
||||
|
// Created by Md. Yamin on 4/25/20. |
||||
|
// Copyright © 2020 Md. Yamin. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
import Foundation |
||||
|
import UIKit |
||||
|
import SwiftUI |
||||
|
import Combine |
||||
|
import WebKit |
||||
|
|
||||
|
// MARK: - WebViewHandlerDelegate |
||||
|
// For printing values received from web app |
||||
|
protocol WebViewHandlerDelegate { |
||||
|
func receivedJsonValueFromWebView(value: [String: Any?]) |
||||
|
func receivedStringValueFromWebView(value: String) |
||||
|
} |
||||
|
|
||||
|
// MARK: - WebView |
||||
|
struct WebView: UIViewRepresentable, WebViewHandlerDelegate { |
||||
|
func receivedJsonValueFromWebView(value: [String : Any?]) { |
||||
|
print("JSON value received from web is: \(value)") |
||||
|
} |
||||
|
|
||||
|
func receivedStringValueFromWebView(value: String) { |
||||
|
print("String value received from web is: \(value)") |
||||
|
} |
||||
|
|
||||
|
// Viewmodel object |
||||
|
@ObservedObject var viewModel: WebViewModel |
||||
|
@State var url: String |
||||
|
|
||||
|
// Make a coordinator to co-ordinate with WKWebView's default delegate functions |
||||
|
func makeCoordinator() -> Coordinator { |
||||
|
Coordinator(self) |
||||
|
} |
||||
|
|
||||
|
func makeUIView(context: Context) -> WKWebView { |
||||
|
// Enable javascript in WKWebView |
||||
|
let preferences = WKPreferences() |
||||
|
preferences.javaScriptEnabled = true |
||||
|
|
||||
|
let configuration = WKWebViewConfiguration() |
||||
|
// Here "iOSNative" is our delegate name that we pushed to the website that is being loaded |
||||
|
let coordinator = self.makeCoordinator() |
||||
|
configuration.userContentController.add(coordinator, name: "jsError") |
||||
|
configuration.userContentController.add(coordinator, name: "openDocument") |
||||
|
configuration.preferences = preferences |
||||
|
|
||||
|
let webView = WKWebView(frame: CGRect.zero, configuration: configuration) |
||||
|
webView.navigationDelegate = context.coordinator |
||||
|
webView.allowsBackForwardNavigationGestures = true |
||||
|
webView.scrollView.isScrollEnabled = true |
||||
|
|
||||
|
viewModel.httpCookieStore = configuration.websiteDataStore.httpCookieStore |
||||
|
return webView |
||||
|
} |
||||
|
|
||||
|
func updateUIView(_ webView: WKWebView, context: Context) { |
||||
|
webView.load(URLRequest(url: URL(string: url)!)) |
||||
|
} |
||||
|
|
||||
|
class Coordinator : NSObject, WKNavigationDelegate, WKScriptMessageHandler { |
||||
|
var parent: WebView |
||||
|
var delegate: WebViewHandlerDelegate? |
||||
|
var valueSubscriber: AnyCancellable? = nil |
||||
|
var webViewNavigationSubscriber: AnyCancellable? = nil |
||||
|
|
||||
|
init(_ uiWebView: WebView) { |
||||
|
self.parent = uiWebView |
||||
|
self.delegate = parent |
||||
|
} |
||||
|
|
||||
|
deinit { |
||||
|
valueSubscriber?.cancel() |
||||
|
webViewNavigationSubscriber?.cancel() |
||||
|
} |
||||
|
|
||||
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { |
||||
|
// Get the title of loaded webcontent |
||||
|
webView.evaluateJavaScript("document.title") { (response, error) in |
||||
|
if let error = error { |
||||
|
print("Error getting title") |
||||
|
print(error.localizedDescription) |
||||
|
} |
||||
|
|
||||
|
guard let title = response as? String else { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
self.parent.viewModel.showWebTitle.send(title) |
||||
|
} |
||||
|
|
||||
|
/* An observer that observes 'viewModel.valuePublisher' to get value from TextField and |
||||
|
pass that value to web app by calling JavaScript function */ |
||||
|
valueSubscriber = parent.viewModel.valuePublisher.receive(on: RunLoop.main).sink(receiveValue: { value in |
||||
|
|
||||
|
do { |
||||
|
let url = NetworkManager.sharedInstance.getDownloadJs() |
||||
|
let js = try String(contentsOf: url, encoding: .utf8) |
||||
|
//let j = js.replacingOccurrences(of: "(absoluteUrl)", with: absoluteUrl) |
||||
|
webView.evaluateJavaScript(js) { (result, err) in |
||||
|
if (err != nil) { |
||||
|
debugPrint("JS ERR: \(String(describing: err))") |
||||
|
} |
||||
|
} |
||||
|
} catch { |
||||
|
print(error) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// Page loaded so no need to show loader anymore |
||||
|
self.parent.viewModel.showLoader.send(false) |
||||
|
} |
||||
|
|
||||
|
/* Here I implemented most of the WKWebView's delegate functions so that you can know them and |
||||
|
can use them in different necessary purposes */ |
||||
|
|
||||
|
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { |
||||
|
// Hides loader |
||||
|
parent.viewModel.showLoader.send(false) |
||||
|
} |
||||
|
|
||||
|
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { |
||||
|
// Hides loader |
||||
|
parent.viewModel.showLoader.send(false) |
||||
|
} |
||||
|
|
||||
|
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { |
||||
|
// Shows loader |
||||
|
parent.viewModel.showLoader.send(true) |
||||
|
} |
||||
|
|
||||
|
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { |
||||
|
// Shows loader |
||||
|
parent.viewModel.showLoader.send(true) |
||||
|
self.webViewNavigationSubscriber = self.parent.viewModel.webViewNavigationPublisher.receive(on: RunLoop.main).sink(receiveValue: { navigation in |
||||
|
switch navigation { |
||||
|
case .backward: |
||||
|
if webView.canGoBack { |
||||
|
webView.goBack() |
||||
|
} |
||||
|
case .forward: |
||||
|
if webView.canGoForward { |
||||
|
webView.goForward() |
||||
|
} |
||||
|
case .reload: |
||||
|
webView.reload() |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// This function is essential for intercepting every navigation in the webview |
||||
|
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { |
||||
|
// Suppose you don't want your user to go a restricted site |
||||
|
// Here you can get many information about new url from 'navigationAction.request.description' |
||||
|
if let host = navigationAction.request.url?.host { |
||||
|
if host == "restricted.com" { |
||||
|
// This cancels the navigation |
||||
|
decisionHandler(.cancel) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
// This allows the navigation |
||||
|
decisionHandler(.allow) |
||||
|
} |
||||
|
|
||||
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { |
||||
|
// Make sure that your passed delegate is called |
||||
|
print(message) |
||||
|
if message.name == "jsError" { |
||||
|
if let body = message.body as? String { |
||||
|
print(body) |
||||
|
} |
||||
|
} |
||||
|
if message.name == "openDocument" { |
||||
|
do { |
||||
|
let data = (message.body as! String).data(using: .utf8) |
||||
|
let jsonObject = try JSONSerialization.jsonObject(with: data!, options: []) |
||||
|
parent.viewModel.downloadPublisher.send(jsonObject as! [String]) |
||||
|
} catch { |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
// |
||||
|
// File.swift |
||||
|
// SwiftUIWebView |
||||
|
// |
||||
|
// Created by Md. Yamin on 4/25/20. |
||||
|
// Copyright © 2020 Md. Yamin. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
import Foundation |
||||
|
import Combine |
||||
|
import WebKit |
||||
|
|
||||
|
class WebViewModel: ObservableObject { |
||||
|
var webViewNavigationPublisher = PassthroughSubject<WebViewNavigation, Never>() |
||||
|
var showWebTitle = PassthroughSubject<String, Never>() |
||||
|
var showLoader = PassthroughSubject<Bool, Never>() |
||||
|
var valuePublisher = PassthroughSubject<String, Never>() |
||||
|
var downloadPublisher = PassthroughSubject<[String], Never>() |
||||
|
var httpCookieStore: WKHTTPCookieStore? |
||||
|
} |
||||
|
|
||||
|
// For identifiying WebView's forward and backward navigation |
||||
|
enum WebViewNavigation { |
||||
|
case backward, forward, reload |
||||
|
} |
||||
|
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue