mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
391 lines
15 KiB
Swift
391 lines
15 KiB
Swift
import UIKit
|
|
import Social
|
|
import MobileCoreServices
|
|
import Photos
|
|
|
|
public class SharedMediaFile: Codable {
|
|
var path: String
|
|
var mimeType: String?
|
|
var thumbnail: String? // video thumbnail
|
|
var duration: Double? // video duration in milliseconds
|
|
var message: String? // post message
|
|
var type: SharedMediaType
|
|
|
|
public init(
|
|
path: String,
|
|
mimeType: String? = nil,
|
|
thumbnail: String? = nil,
|
|
duration: Double? = nil,
|
|
message: String?=nil,
|
|
type: SharedMediaType) {
|
|
self.path = path
|
|
self.mimeType = mimeType
|
|
self.thumbnail = thumbnail
|
|
self.duration = duration
|
|
self.message = message
|
|
self.type = type
|
|
}
|
|
}
|
|
|
|
public enum SharedMediaType: String, Codable, CaseIterable {
|
|
case image
|
|
case video
|
|
case text
|
|
case file
|
|
case url
|
|
|
|
public var toUTTypeIdentifier: String {
|
|
if #available(iOS 14.0, *) {
|
|
switch self {
|
|
case .image:
|
|
return UTType.image.identifier
|
|
case .video:
|
|
return UTType.movie.identifier
|
|
case .text:
|
|
return UTType.text.identifier
|
|
case .file:
|
|
return UTType.fileURL.identifier
|
|
case .url:
|
|
return UTType.url.identifier
|
|
}
|
|
}
|
|
switch self {
|
|
case .image:
|
|
return "public.image"
|
|
case .video:
|
|
return "public.movie"
|
|
case .text:
|
|
return "public.text"
|
|
case .file:
|
|
return "public.file-url"
|
|
case .url:
|
|
return "public.url"
|
|
}
|
|
}
|
|
}
|
|
|
|
let kSchemePrefix = "ShareMedia"
|
|
let kUserDefaultsKey = "ShareKey"
|
|
let kUserDefaultsMessageKey = "ShareMessageKey"
|
|
let kAppGroupIdKey = "AppGroupId"
|
|
|
|
class ShareViewController: SLComposeServiceViewController {
|
|
var hostAppBundleIdentifier = "com.jiaqi.hacki"
|
|
var appGroupId = "group.com.jiaqi.hacki"
|
|
var sharedMedia: [SharedMediaFile] = []
|
|
|
|
/// Override this method to return false if you don't want to redirect to host app automatically
|
|
/// Default is true
|
|
open func shouldAutoRedirect() -> Bool {
|
|
return true
|
|
}
|
|
|
|
open override func isContentValid() -> Bool {
|
|
return true
|
|
}
|
|
|
|
open override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
// load group and app id from build info
|
|
loadIds()
|
|
}
|
|
|
|
// Redirect to host app when user click on Post
|
|
open override func didSelectPost() {
|
|
saveAndRedirect(message: contentText)
|
|
}
|
|
|
|
open override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
|
|
if let content = extensionContext!.inputItems[0] as? NSExtensionItem {
|
|
if let contents = content.attachments {
|
|
for (index, attachment) in (contents).enumerated() {
|
|
for type in SharedMediaType.allCases {
|
|
if attachment.hasItemConformingToTypeIdentifier(type.toUTTypeIdentifier) {
|
|
attachment.loadItem(forTypeIdentifier: type.toUTTypeIdentifier) { [weak self] data, error in
|
|
guard let this = self, error == nil else {
|
|
self?.dismissWithError()
|
|
return
|
|
}
|
|
switch type {
|
|
case .text:
|
|
if let text = data as? String {
|
|
this.handleMedia(forLiteral: text,
|
|
type: type,
|
|
index: index,
|
|
content: content)
|
|
}
|
|
case .url:
|
|
if let url = data as? URL {
|
|
this.handleMedia(forLiteral: url.absoluteString,
|
|
type: type,
|
|
index: index,
|
|
content: content)
|
|
}
|
|
default:
|
|
if let url = data as? URL {
|
|
this.handleMedia(forFile: url,
|
|
type: type,
|
|
index: index,
|
|
content: content)
|
|
}
|
|
else if let image = data as? UIImage {
|
|
this.handleMedia(forUIImage: image,
|
|
type: type,
|
|
index: index,
|
|
content: content)
|
|
}
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
open override func configurationItems() -> [Any]! {
|
|
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
|
|
return []
|
|
}
|
|
|
|
private func loadIds() {
|
|
// loading Share extension App Id
|
|
let shareExtensionAppBundleIdentifier = Bundle.main.bundleIdentifier!
|
|
|
|
|
|
// extract host app bundle id from ShareExtension id
|
|
// by default it's <hostAppBundleIdentifier>.<ShareExtension>
|
|
// for example: "com.kasem.sharing.Share-Extension" -> com.kasem.sharing
|
|
let lastIndexOfPoint = shareExtensionAppBundleIdentifier.lastIndex(of: ".")
|
|
hostAppBundleIdentifier = String(shareExtensionAppBundleIdentifier[..<lastIndexOfPoint!])
|
|
let defaultAppGroupId = "group.\(hostAppBundleIdentifier)"
|
|
|
|
|
|
// loading custom AppGroupId from Build Settings or use group.<hostAppBundleIdentifier>
|
|
let customAppGroupId = Bundle.main.object(forInfoDictionaryKey: kAppGroupIdKey) as? String
|
|
|
|
appGroupId = customAppGroupId ?? defaultAppGroupId
|
|
}
|
|
|
|
|
|
private func handleMedia(forLiteral item: String, type: SharedMediaType, index: Int, content: NSExtensionItem) {
|
|
sharedMedia.append(SharedMediaFile(
|
|
path: item,
|
|
mimeType: type == .text ? "text/plain": nil,
|
|
type: type
|
|
))
|
|
if index == (content.attachments?.count ?? 0) - 1 {
|
|
if shouldAutoRedirect() {
|
|
saveAndRedirect()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleMedia(forUIImage image: UIImage, type: SharedMediaType, index: Int, content: NSExtensionItem){
|
|
let tempPath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)!.appendingPathComponent("TempImage.png")
|
|
if self.writeTempFile(image, to: tempPath) {
|
|
let newPathDecoded = tempPath.absoluteString.removingPercentEncoding!
|
|
sharedMedia.append(SharedMediaFile(
|
|
path: newPathDecoded,
|
|
mimeType: type == .image ? "image/png": nil,
|
|
type: type
|
|
))
|
|
}
|
|
if index == (content.attachments?.count ?? 0) - 1 {
|
|
if shouldAutoRedirect() {
|
|
saveAndRedirect()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleMedia(forFile url: URL, type: SharedMediaType, index: Int, content: NSExtensionItem) {
|
|
let fileName = getFileName(from: url, type: type)
|
|
let newPath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)!.appendingPathComponent(fileName)
|
|
|
|
if copyFile(at: url, to: newPath) {
|
|
// The path should be decoded because Flutter is not expecting url encoded file names
|
|
let newPathDecoded = newPath.absoluteString.removingPercentEncoding!;
|
|
if type == .video {
|
|
// Get video thumbnail and duration
|
|
if let videoInfo = getVideoInfo(from: url) {
|
|
let thumbnailPathDecoded = videoInfo.thumbnail?.removingPercentEncoding;
|
|
sharedMedia.append(SharedMediaFile(
|
|
path: newPathDecoded,
|
|
mimeType: url.mimeType(),
|
|
thumbnail: thumbnailPathDecoded,
|
|
duration: videoInfo.duration,
|
|
type: type
|
|
))
|
|
}
|
|
} else {
|
|
sharedMedia.append(SharedMediaFile(
|
|
path: newPathDecoded,
|
|
mimeType: url.mimeType(),
|
|
type: type
|
|
))
|
|
}
|
|
}
|
|
|
|
if index == (content.attachments?.count ?? 0) - 1 {
|
|
if shouldAutoRedirect() {
|
|
saveAndRedirect()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Save shared media and redirect to host app
|
|
private func saveAndRedirect(message: String? = nil) {
|
|
let userDefaults = UserDefaults(suiteName: appGroupId)
|
|
userDefaults?.set(toData(data: sharedMedia), forKey: kUserDefaultsKey)
|
|
userDefaults?.set(message, forKey: kUserDefaultsMessageKey)
|
|
userDefaults?.synchronize()
|
|
redirectToHostApp()
|
|
}
|
|
|
|
private func redirectToHostApp() {
|
|
// ids may not loaded yet so we need loadIds here too
|
|
loadIds()
|
|
let url = URL(string: "\(kSchemePrefix)-\(hostAppBundleIdentifier):share")
|
|
var responder = self as UIResponder?
|
|
|
|
if #available(iOS 18.0, *) {
|
|
while responder != nil {
|
|
if let application = responder as? UIApplication {
|
|
application.open(url!, options: [:], completionHandler: nil)
|
|
}
|
|
responder = responder?.next
|
|
}
|
|
} else {
|
|
let selectorOpenURL = sel_registerName("openURL:")
|
|
|
|
while (responder != nil) {
|
|
if (responder?.responds(to: selectorOpenURL))! {
|
|
_ = responder?.perform(selectorOpenURL, with: url)
|
|
}
|
|
responder = responder!.next
|
|
}
|
|
}
|
|
|
|
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
|
}
|
|
|
|
private func dismissWithError() {
|
|
print("[ERROR] Error loading data!")
|
|
let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert)
|
|
|
|
let action = UIAlertAction(title: "Error", style: .cancel) { _ in
|
|
self.dismiss(animated: true, completion: nil)
|
|
}
|
|
|
|
alert.addAction(action)
|
|
present(alert, animated: true, completion: nil)
|
|
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
|
}
|
|
|
|
private func getFileName(from url: URL, type: SharedMediaType) -> String {
|
|
var name = url.lastPathComponent
|
|
if name.isEmpty {
|
|
switch type {
|
|
case .image:
|
|
name = UUID().uuidString + ".png"
|
|
case .video:
|
|
name = UUID().uuidString + ".mp4"
|
|
case .text:
|
|
name = UUID().uuidString + ".txt"
|
|
default:
|
|
name = UUID().uuidString
|
|
}
|
|
}
|
|
return name
|
|
}
|
|
|
|
private func writeTempFile(_ image: UIImage, to dstURL: URL) -> Bool {
|
|
do {
|
|
if FileManager.default.fileExists(atPath: dstURL.path) {
|
|
try FileManager.default.removeItem(at: dstURL)
|
|
}
|
|
let pngData = image.pngData();
|
|
try pngData?.write(to: dstURL);
|
|
return true;
|
|
} catch (let error){
|
|
print("Cannot write to temp file: \(error)");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private func copyFile(at srcURL: URL, to dstURL: URL) -> Bool {
|
|
do {
|
|
if FileManager.default.fileExists(atPath: dstURL.path) {
|
|
try FileManager.default.removeItem(at: dstURL)
|
|
}
|
|
try FileManager.default.copyItem(at: srcURL, to: dstURL)
|
|
} catch (let error) {
|
|
print("Cannot copy item at \(srcURL) to \(dstURL): \(error)")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private func getVideoInfo(from url: URL) -> (thumbnail: String?, duration: Double)? {
|
|
let asset = AVAsset(url: url)
|
|
let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded()
|
|
let thumbnailPath = getThumbnailPath(for: url)
|
|
|
|
if FileManager.default.fileExists(atPath: thumbnailPath.path) {
|
|
return (thumbnail: thumbnailPath.absoluteString, duration: duration)
|
|
}
|
|
|
|
var saved = false
|
|
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
|
|
assetImgGenerate.appliesPreferredTrackTransform = true
|
|
// let scale = UIScreen.main.scale
|
|
assetImgGenerate.maximumSize = CGSize(width: 360, height: 360)
|
|
do {
|
|
let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: 1), actualTime: nil)
|
|
try UIImage(cgImage: img).pngData()?.write(to: thumbnailPath)
|
|
saved = true
|
|
} catch {
|
|
saved = false
|
|
}
|
|
|
|
return saved ? (thumbnail: thumbnailPath.absoluteString, duration: duration): nil
|
|
}
|
|
|
|
private func getThumbnailPath(for url: URL) -> URL {
|
|
let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "")
|
|
let path = FileManager.default
|
|
.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)!
|
|
.appendingPathComponent("\(fileName).jpg")
|
|
return path
|
|
}
|
|
|
|
private func toData(data: [SharedMediaFile]) -> Data {
|
|
let encodedData = try? JSONEncoder().encode(data)
|
|
return encodedData!
|
|
}
|
|
}
|
|
|
|
extension URL {
|
|
public func mimeType() -> String {
|
|
if #available(iOS 14.0, *) {
|
|
if let mimeType = UTType(filenameExtension: self.pathExtension)?.preferredMIMEType {
|
|
return mimeType
|
|
}
|
|
} else {
|
|
if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, self.pathExtension as NSString, nil)?.takeRetainedValue() {
|
|
if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
|
|
return mimetype as String
|
|
}
|
|
}
|
|
}
|
|
|
|
return "application/octet-stream"
|
|
}
|
|
}
|