8 changed files with 591 additions and 6 deletions
-
24kplayer.xcodeproj/project.pbxproj
-
2kplayer/core/MediaItem.swift
-
64kplayer/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