mirror of
https://github.com/RSSNext/Folo.git
synced 2026-10-26 10:25:32 +08:00
- Refactored WebViewManager to improve lifecycle management and state handling. - Introduced a debug panel for monitoring WebView state and actions in development mode. - Updated JavaScript injection methods to use a centralized dispatch system for better maintainability. - Improved handling of local HTML loading and state replay after process termination. - Added new functions for debugging, including state retrieval and flushing pending scripts. This update aims to enhance the overall performance and usability of the WebView component, providing developers with better tools for debugging and state management. Signed-off-by: Innei <tukon479@gmail.com>
205 lines
7.0 KiB
Swift
205 lines
7.0 KiB
Swift
//
|
|
// HelperModule.swift
|
|
// Pods
|
|
//
|
|
// Created by Innei on 2025/2/7.
|
|
//
|
|
import ExpoModulesCore
|
|
import UIKit
|
|
|
|
public class HelperModule: Module {
|
|
public func definition() -> ExpoModulesCore.ModuleDefinition {
|
|
Name("Helper")
|
|
|
|
AsyncFunction("openLink") { (urlString: String, promise: Promise) in
|
|
guard let url = URL(string: urlString) else {
|
|
return
|
|
}
|
|
DispatchQueue.main.async {
|
|
guard let rootVC = Utils.getRootVC() else { return }
|
|
|
|
let onDismiss = {
|
|
promise.resolve(["type": "dismiss"])
|
|
}
|
|
|
|
WebViewManager.presentModalWebView(url: url, from: rootVC, onDismiss: onDismiss)
|
|
}
|
|
}
|
|
|
|
Function("scrollToTop") { (reactTag: Int) in
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let bridge = self?.appContext?.reactBridge else { return }
|
|
|
|
if let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: reactTag)) {
|
|
let scrollView = self?.findUIScrollView(view: sourceView)
|
|
guard let scrollView = scrollView else {
|
|
return
|
|
}
|
|
scrollView.scrollToTopIfPossible(animated: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
AsyncFunction("isScrollToEnd") { (reactTag: Int, promise: Promise) in
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let bridge = self?.appContext?.reactBridge else { return }
|
|
|
|
if let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: reactTag)) {
|
|
let scrollView = self?.findUIScrollView(view: sourceView)
|
|
guard let scrollView = scrollView else {
|
|
return
|
|
}
|
|
let contentHeight = scrollView.contentSize.height
|
|
let scrollViewHeight = scrollView.bounds.size.height
|
|
let contentOffsetY = scrollView.contentOffset.y
|
|
|
|
let bottomOffset = contentHeight - scrollViewHeight
|
|
|
|
promise.resolve(contentOffsetY >= bottomOffset - 1.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
Function("saveImageByHandle") { (reactTag: Int) in
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let bridge = self?.appContext?.reactBridge else { return }
|
|
|
|
if let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: reactTag)) {
|
|
let imageView = self?.findUIImageView(view: sourceView)
|
|
guard let imageView = imageView else {
|
|
return
|
|
}
|
|
guard let image = imageView.image else { return }
|
|
UIImageWriteToSavedPhotosAlbum(image, self, #selector(HelperModule.image), nil)
|
|
Toast.show(options: .init(type: .success, title: "Saved to photos"))
|
|
}
|
|
}
|
|
}
|
|
|
|
Function("shareImageByHandle") { (reactTag: Int, url: String?) in
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let bridge = self?.appContext?.reactBridge else { return }
|
|
if let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: reactTag)) {
|
|
let imageView = self?.findUIImageView(view: sourceView)
|
|
guard let imageView = imageView else {
|
|
return
|
|
}
|
|
guard let image = imageView.image else { return }
|
|
let activityViewController = UIActivityViewController(
|
|
activityItems: [
|
|
image.asActivityItemSource(
|
|
url: try? URL(string: url ?? "")
|
|
)
|
|
], applicationActivities: nil)
|
|
activityViewController.popoverPresentationController?.sourceView = sourceView
|
|
activityViewController.popoverPresentationController?.sourceRect = sourceView.bounds
|
|
activityViewController.popoverPresentationController?.permittedArrowDirections = .any
|
|
activityViewController.popoverPresentationController?.permittedArrowDirections = .any
|
|
|
|
Utils.getRootVC()?.present(activityViewController, animated: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
AsyncFunction("getBase64FromImageViewByHandle") { (reactTag: Int, promise: Promise) in
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let bridge = self?.appContext?.reactBridge else { return }
|
|
|
|
if let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: reactTag)) {
|
|
let imageView = self?.findUIImageView(view: sourceView)
|
|
guard let imageView = imageView else {
|
|
promise.reject(
|
|
NSError(
|
|
domain: "HelperModule", code: 0,
|
|
userInfo: [NSLocalizedDescriptionKey: "Image view not found"]))
|
|
return
|
|
}
|
|
let base64 = self?.getBase64FromImageView(imageView: imageView)
|
|
promise.resolve(["base64": base64])
|
|
}
|
|
}
|
|
}
|
|
|
|
Function("copyImageByHandle") { (reactTag: Int) in
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let bridge = self?.appContext?.reactBridge else { return }
|
|
|
|
if let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: reactTag)) {
|
|
let imageView = self?.findUIImageView(view: sourceView)
|
|
guard let imageView = imageView else {
|
|
return
|
|
}
|
|
guard let image = imageView.image else { return }
|
|
guard let imageData = image.pngData() else { return }
|
|
UIPasteboard.general.setData(imageData, forPasteboardType: "public.png")
|
|
|
|
Toast.show(options: .init(type: .success, title: "Image copied to clipboard"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func getBase64FromImageView(imageView: UIImageView) -> String? {
|
|
guard let image = imageView.image else { return nil }
|
|
guard let imageData = image.pngData() else { return nil }
|
|
|
|
let base64String = imageData.base64EncodedString(options: .lineLength64Characters)
|
|
return base64String
|
|
}
|
|
|
|
@objc func image(
|
|
_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer?
|
|
) {
|
|
if let error = error {
|
|
Toast.show(options: .init(type: .error, title: "Save image failed"))
|
|
} else {
|
|
Toast.show(options: .init(type: .success, title: "Saved to photos"))
|
|
}
|
|
}
|
|
|
|
private func findUIScrollView(view: UIView?) -> UIScrollView? {
|
|
return findUIViewOfType(view: view)
|
|
}
|
|
|
|
private func findUIImageView(view: UIView?) -> UIImageView? {
|
|
return findUIViewOfType(view: view)
|
|
}
|
|
|
|
private func findUIViewOfType<T: UIView>(view: UIView?) -> T? {
|
|
guard let view = view else {
|
|
return nil
|
|
}
|
|
if let view = view as? T {
|
|
return view
|
|
}
|
|
|
|
let subviews = view.subviews
|
|
for subview in subviews {
|
|
if let targetView = findUIViewOfType(view: subview) as T? {
|
|
return targetView
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
extension UIScrollView {
|
|
func scrollToTopIfPossible(animated: Bool) {
|
|
let encodedSelector = "X3Njcm9sbFRvVG9wSWZQb3NzaWJsZTo=" // "_scrollToTopIfPossible:"
|
|
|
|
if let decodedData = Data(base64Encoded: encodedSelector),
|
|
let decodedString = String(data: decodedData, encoding: .utf8)
|
|
{
|
|
let selector = NSSelectorFromString(decodedString)
|
|
if responds(to: selector) {
|
|
perform(selector, with: animated)
|
|
} else {
|
|
print("UIScrollView does not respond to decoded method")
|
|
setContentOffset(.zero, animated: animated)
|
|
}
|
|
} else {
|
|
setContentOffset(.zero, animated: animated)
|
|
}
|
|
}
|
|
}
|