Fix share on ipad, improve share file names
This commit is contained in:
parent
2bc97856af
commit
646550575d
|
@ -65,6 +65,9 @@ class MainActivity: FlutterActivity() {
|
||||||
"active.setRemoteForTunnel" -> activeSetRemoteForTunnel(call, result)
|
"active.setRemoteForTunnel" -> activeSetRemoteForTunnel(call, result)
|
||||||
"active.closeTunnel" -> activeCloseTunnel(call, result)
|
"active.closeTunnel" -> activeCloseTunnel(call, result)
|
||||||
|
|
||||||
|
"share" -> Share.share(call, result)
|
||||||
|
"shareFile" -> Share.shareFile(call, result)
|
||||||
|
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
package net.defined.mobile_nebula
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ResolveInfo
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class Share {
|
||||||
|
companion object {
|
||||||
|
fun share(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val title = call.argument<String>("title")
|
||||||
|
val text = call.argument<String>("text")
|
||||||
|
val filename = call.argument<String>("filename")
|
||||||
|
|
||||||
|
if (filename == null || filename.isEmpty()) {
|
||||||
|
return result.error("filename was not provided", null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val context = MainActivity!!.getContext()!!
|
||||||
|
val cacheDir = context.cacheDir.resolve("share")
|
||||||
|
cacheDir.deleteRecursively()
|
||||||
|
cacheDir.mkdir()
|
||||||
|
val newFile = cacheDir.resolve(filename!!)
|
||||||
|
newFile.delete()
|
||||||
|
newFile.writeText(text ?: "")
|
||||||
|
pop(title, newFile, result)
|
||||||
|
|
||||||
|
} catch (err: Exception) {
|
||||||
|
Log.println(Log.ERROR, "", "Share: Error")
|
||||||
|
result.error(err.message, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shareFile(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val title = call.argument<String>("title")
|
||||||
|
val filename = call.argument<String>("filename")
|
||||||
|
val filePath = call.argument<String>("filePath")
|
||||||
|
|
||||||
|
if (filename == null || filename.isEmpty()) {
|
||||||
|
result.error("filename was not provided", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filePath == null || filePath.isEmpty()) {
|
||||||
|
result.error("filePath was not provided", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = File(filePath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val context = MainActivity!!.getContext()!!
|
||||||
|
val cacheDir = context.cacheDir.resolve("share")
|
||||||
|
cacheDir.deleteRecursively()
|
||||||
|
cacheDir.mkdir()
|
||||||
|
val newFile = cacheDir.resolve(filename!!)
|
||||||
|
newFile.delete()
|
||||||
|
file.copyTo(newFile)
|
||||||
|
|
||||||
|
pop(title, newFile, result)
|
||||||
|
|
||||||
|
} catch (err: Exception) {
|
||||||
|
Log.println(Log.ERROR, "", "Share: Error")
|
||||||
|
result.error(err.message, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pop(title: String?, file: File, result: MethodChannel.Result) {
|
||||||
|
if (title == null || title.isEmpty()) {
|
||||||
|
result.error("title was not provided", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val context = MainActivity!!.getContext()!!
|
||||||
|
|
||||||
|
val fileUri = FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", file)
|
||||||
|
val intent = Intent()
|
||||||
|
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
intent.action = Intent.ACTION_SEND
|
||||||
|
intent.type = "*/*"
|
||||||
|
|
||||||
|
intent.putExtra(Intent.EXTRA_SUBJECT, title)
|
||||||
|
intent.putExtra(Intent.EXTRA_STREAM, fileUri)
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
|
||||||
|
val chooserIntent = Intent.createChooser(intent, title)
|
||||||
|
chooserIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
chooserIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
|
||||||
|
val resInfoList: List<ResolveInfo> = context.packageManager.queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
for (resolveInfo in resInfoList) {
|
||||||
|
print(fileUri)
|
||||||
|
val packageName: String = resolveInfo.activityInfo.packageName
|
||||||
|
context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(chooserIntent)
|
||||||
|
|
||||||
|
} catch (err: Exception) {
|
||||||
|
Log.println(Log.ERROR, "", "Share: Error")
|
||||||
|
return result.error(err.message, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<paths>
|
<paths>
|
||||||
<files-path name="sites" path="."/>
|
<cache-path name="tmp cache" path="share/" />
|
||||||
<external-path name="external-sites" path="." />
|
|
||||||
</paths>
|
</paths>
|
||||||
|
|
|
@ -41,8 +41,6 @@ PODS:
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_plugin_android_lifecycle (0.0.1):
|
- flutter_plugin_android_lifecycle (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_share (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- MMWormhole (2.0.0):
|
- MMWormhole (2.0.0):
|
||||||
- MMWormhole/Core (= 2.0.0)
|
- MMWormhole/Core (= 2.0.0)
|
||||||
- MMWormhole/Core (2.0.0)
|
- MMWormhole/Core (2.0.0)
|
||||||
|
@ -74,7 +72,6 @@ DEPENDENCIES:
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_plugin_android_lifecycle (from `.symlinks/plugins/flutter_plugin_android_lifecycle/ios`)
|
- flutter_plugin_android_lifecycle (from `.symlinks/plugins/flutter_plugin_android_lifecycle/ios`)
|
||||||
- flutter_share (from `.symlinks/plugins/flutter_share/ios`)
|
|
||||||
- MMWormhole (~> 2.0.0)
|
- MMWormhole (~> 2.0.0)
|
||||||
- package_info (from `.symlinks/plugins/package_info/ios`)
|
- package_info (from `.symlinks/plugins/package_info/ios`)
|
||||||
- path_provider (from `.symlinks/plugins/path_provider/ios`)
|
- path_provider (from `.symlinks/plugins/path_provider/ios`)
|
||||||
|
@ -104,8 +101,6 @@ EXTERNAL SOURCES:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
:path: ".symlinks/plugins/flutter_plugin_android_lifecycle/ios"
|
:path: ".symlinks/plugins/flutter_plugin_android_lifecycle/ios"
|
||||||
flutter_share:
|
|
||||||
:path: ".symlinks/plugins/flutter_share/ios"
|
|
||||||
package_info:
|
package_info:
|
||||||
:path: ".symlinks/plugins/package_info/ios"
|
:path: ".symlinks/plugins/package_info/ios"
|
||||||
path_provider:
|
path_provider:
|
||||||
|
@ -129,7 +124,6 @@ SPEC CHECKSUMS:
|
||||||
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
|
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
|
||||||
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
|
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
|
||||||
flutter_plugin_android_lifecycle: dc0b544e129eebb77a6bfb1239d4d1c673a60a35
|
flutter_plugin_android_lifecycle: dc0b544e129eebb77a6bfb1239d4d1c673a60a35
|
||||||
flutter_share: 4be0208963c60b537e6255ed2ce1faae61cd9ac2
|
|
||||||
MMWormhole: 0cd3fd35a9118b2e2d762b499f54eeaace0be791
|
MMWormhole: 0cd3fd35a9118b2e2d762b499f54eeaace0be791
|
||||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||||
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
|
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
43AA89572444DA6500EDC39C /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA89562444DA6500EDC39C /* PacketTunnelProvider.swift */; };
|
43AA89572444DA6500EDC39C /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA89562444DA6500EDC39C /* PacketTunnelProvider.swift */; };
|
||||||
43AA895C2444DA6500EDC39C /* NebulaNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 43AA89542444DA6500EDC39C /* NebulaNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
43AA895C2444DA6500EDC39C /* NebulaNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 43AA89542444DA6500EDC39C /* NebulaNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
43AA89622444DAA500EDC39C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43AA894E2444D8BC00EDC39C /* NetworkExtension.framework */; };
|
43AA89622444DAA500EDC39C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43AA894E2444D8BC00EDC39C /* NetworkExtension.framework */; };
|
||||||
|
43AD63F424EB3802000FB47E /* Share.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AD63F324EB3802000FB47E /* Share.swift */; };
|
||||||
4CF2F06A02A63B862C9F6F03 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384887B4785D38431E800D3A /* Pods_Runner.framework */; };
|
4CF2F06A02A63B862C9F6F03 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384887B4785D38431E800D3A /* Pods_Runner.framework */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
|
@ -76,6 +77,7 @@
|
||||||
43AA89562444DA6500EDC39C /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
|
43AA89562444DA6500EDC39C /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
|
||||||
43AA89582444DA6500EDC39C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
43AA89582444DA6500EDC39C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
43AA89592444DA6500EDC39C /* NebulaNetworkExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NebulaNetworkExtension.entitlements; sourceTree = "<group>"; };
|
43AA89592444DA6500EDC39C /* NebulaNetworkExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NebulaNetworkExtension.entitlements; sourceTree = "<group>"; };
|
||||||
|
43AD63F324EB3802000FB47E /* Share.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Share.swift; sourceTree = "<group>"; };
|
||||||
43B66ECA245A0C8400B18C36 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
|
43B66ECA245A0C8400B18C36 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
|
||||||
43B66ECC245A146300B18C36 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
|
43B66ECC245A146300B18C36 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
|
||||||
43B828DA249C08DC00CA229C /* MMWormhole.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MMWormhole.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
43B828DA249C08DC00CA229C /* MMWormhole.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MMWormhole.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -188,6 +190,7 @@
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
43871C9C2444E2EC004F9075 /* Sites.swift */,
|
43871C9C2444E2EC004F9075 /* Sites.swift */,
|
||||||
|
43AD63F324EB3802000FB47E /* Share.swift */,
|
||||||
);
|
);
|
||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -418,6 +421,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
43AD63F424EB3802000FB47E /* Share.swift in Sources */,
|
||||||
43871C9D2444E2EC004F9075 /* Sites.swift in Sources */,
|
43871C9D2444E2EC004F9075 /* Sites.swift in Sources */,
|
||||||
437F725F2469B4B000A0C4B9 /* Site.swift in Sources */,
|
437F725F2469B4B000A0C4B9 /* Site.swift in Sources */,
|
||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
|
|
@ -51,6 +51,9 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
||||||
case "active.setRemoteForTunnel": self.activeSetRemoteForTunnel(call: call, result: result)
|
case "active.setRemoteForTunnel": self.activeSetRemoteForTunnel(call: call, result: result)
|
||||||
case "active.closeTunnel": self.activeCloseTunnel(call: call, result: result)
|
case "active.closeTunnel": self.activeCloseTunnel(call: call, result: result)
|
||||||
|
|
||||||
|
case "share": Share.share(call: call, result: result)
|
||||||
|
case "shareFile": Share.shareFile(call: call, result: result)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
result(FlutterMethodNotImplemented)
|
result(FlutterMethodNotImplemented)
|
||||||
}
|
}
|
||||||
|
@ -86,7 +89,6 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
||||||
guard let config = call.arguments as? String else { return result(NoArgumentsError()) }
|
guard let config = call.arguments as? String else { return result(NoArgumentsError()) }
|
||||||
|
|
||||||
var err: NSError?
|
var err: NSError?
|
||||||
print(config)
|
|
||||||
let yaml = MobileNebulaRenderConfig(config, "<hidden>", &err)
|
let yaml = MobileNebulaRenderConfig(config, "<hidden>", &err)
|
||||||
if (err != nil) {
|
if (err != nil) {
|
||||||
return result(CallFailedError(message: "Error while rendering config", details: err!.localizedDescription))
|
return result(CallFailedError(message: "Error while rendering config", details: err!.localizedDescription))
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Basis of this code comes from https://github.com/lubritto/flutter_share
|
||||||
|
|
||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public class Share {
|
||||||
|
public static func share(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
let args = call.arguments as? [String: Any?]
|
||||||
|
|
||||||
|
let title = args!["title"] as? String
|
||||||
|
let text = args!["text"] as? String
|
||||||
|
let filename = args!["filename"] as? String
|
||||||
|
let tmpDirURL = FileManager.default.temporaryDirectory
|
||||||
|
|
||||||
|
if (filename == nil || filename!.isEmpty) {
|
||||||
|
return result(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let tmpFile = tmpDirURL.appendingPathComponent(filename!)
|
||||||
|
do {
|
||||||
|
try text?.write(to: tmpFile, atomically: true, encoding: .utf8)
|
||||||
|
} catch {
|
||||||
|
//TODO: return error
|
||||||
|
return result(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pop(title: title, file: tmpFile) { pass in
|
||||||
|
let fm = FileManager()
|
||||||
|
do {
|
||||||
|
try fm.removeItem(at: tmpFile)
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return result(pass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func shareFile(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
let args = call.arguments as? [String: Any?]
|
||||||
|
|
||||||
|
let title = args!["title"] as? String
|
||||||
|
let filePath = args!["filePath"] as? String
|
||||||
|
let filename = args!["filename"] as? String
|
||||||
|
|
||||||
|
if (filePath == nil || filePath!.isEmpty) {
|
||||||
|
return result(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmpFile: URL?
|
||||||
|
let fm = FileManager()
|
||||||
|
var realPath = URL(fileURLWithPath: filePath!)
|
||||||
|
|
||||||
|
if (filename != nil && !filename!.isEmpty) {
|
||||||
|
tmpFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename!)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try fm.linkItem(at: URL(fileURLWithPath: filePath!), to: tmpFile!)
|
||||||
|
} catch {
|
||||||
|
//TODO: return error
|
||||||
|
return result(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
realPath = tmpFile!
|
||||||
|
}
|
||||||
|
|
||||||
|
pop(title: title, file: realPath) { pass in
|
||||||
|
if (tmpFile != nil) {
|
||||||
|
do {
|
||||||
|
try fm.removeItem(at: tmpFile!)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
result(pass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func pop(title: String?, file: URL, completion: @escaping ((Bool) -> Void)) {
|
||||||
|
if (title == nil || title!.isEmpty) {
|
||||||
|
return completion(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let activityViewController = UIActivityViewController(activityItems: [file], applicationActivities: nil)
|
||||||
|
|
||||||
|
activityViewController.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems: [Any]?, error: Error?) in
|
||||||
|
completion(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject
|
||||||
|
activityViewController.setValue(title, forKeyPath: "subject")
|
||||||
|
|
||||||
|
// For iPads, fix issue where Exception is thrown by using a popup instead
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||||
|
activityViewController.popoverPresentationController?.sourceView = UIApplication.topViewController()?.view
|
||||||
|
if let view = UIApplication.topViewController()?.view {
|
||||||
|
activityViewController.popoverPresentationController?.permittedArrowDirections = []
|
||||||
|
activityViewController.popoverPresentationController?.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
UIApplication.topViewController()?.present(activityViewController, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIApplication {
|
||||||
|
class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
|
||||||
|
if let navigationController = controller as? UINavigationController {
|
||||||
|
return topViewController(controller: navigationController.visibleViewController)
|
||||||
|
}
|
||||||
|
if let tabController = controller as? UITabBarController {
|
||||||
|
if let selected = tabController.selectedViewController {
|
||||||
|
return topViewController(controller: selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let presented = controller?.presentedViewController {
|
||||||
|
return topViewController(controller: presented)
|
||||||
|
}
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
}
|
|
@ -160,10 +160,10 @@ class _MainScreenState extends State<MainScreen> {
|
||||||
var uuid = Uuid();
|
var uuid = Uuid();
|
||||||
|
|
||||||
var cert = '''-----BEGIN NEBULA CERTIFICATE-----
|
var cert = '''-----BEGIN NEBULA CERTIFICATE-----
|
||||||
CmMKBnBpeGVsNBIJiYCEUID+//8PKLqMivcFMKTzjoYGOiB4iANINzCjLdlQJSj/
|
CmIKBHRlc3QSCoKUoIUMgP7//w8ourrS+QUwjre3iAY6IDbmIX5cwd+UYVhLADLa
|
||||||
vJDd080yggfLgW9hT4a/bhGZekog+W+YEJiV36evX4MueQ+npDzJd3zGg5gialu4
|
A5PwucZPVrNtP0P9NJE0boM2SiBSGzy8bcuFWWK5aVArJGA9VDtLg1HuujBu8lOp
|
||||||
UNGYBP0SQL5bjEyafC0YtETEbrraSfwuFHMvUoi1Kc4XRzTPPvHsEaq3hNNTZtD7
|
VTgklxJAgbI1Xb1C9JC3a1Cnc6NPqWhnw+3VLoDXE9poBav09+zhw5DPDtgvQmxU
|
||||||
Pt3sjH83zTMZfnD/Du3ahsvV0rAXUgc=
|
Sbw6cAF4gPS4e/tZ5Kjc8QEvjk3HDQ==
|
||||||
-----END NEBULA CERTIFICATE-----''';
|
-----END NEBULA CERTIFICATE-----''';
|
||||||
|
|
||||||
var ca = '''-----BEGIN NEBULA CERTIFICATE-----
|
var ca = '''-----BEGIN NEBULA CERTIFICATE-----
|
||||||
|
@ -183,7 +183,9 @@ mUOcsdFcCZiXrj7ryQIG1+WfqA46w71A/lV4nAc=
|
||||||
unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')]
|
unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')]
|
||||||
);
|
);
|
||||||
|
|
||||||
s.key = "-----BEGIN NEBULA X25519 PRIVATE KEY-----\ndYgPb04Bb1xzfgdCfVsKGZrCYe+u5tDWNXKipQBVZ44=\n-----END NEBULA X25519 PRIVATE KEY-----";
|
s.key = '''-----BEGIN NEBULA X25519 PRIVATE KEY-----
|
||||||
|
rmXnR1yvDZi1VPVmnNVY8NMsQpEpbbYlq7rul+ByQvg=
|
||||||
|
-----END NEBULA X25519 PRIVATE KEY-----''';
|
||||||
|
|
||||||
var err = await s.save();
|
var err = await s.save();
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||||
import 'package:flutter_share/flutter_share.dart';
|
|
||||||
import 'package:mobile_nebula/components/SimplePage.dart';
|
import 'package:mobile_nebula/components/SimplePage.dart';
|
||||||
import 'package:mobile_nebula/models/Site.dart';
|
import 'package:mobile_nebula/models/Site.dart';
|
||||||
|
import 'package:mobile_nebula/services/share.dart';
|
||||||
import 'package:mobile_nebula/services/utils.dart';
|
import 'package:mobile_nebula/services/utils.dart';
|
||||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||||
|
|
||||||
|
@ -79,7 +78,7 @@ class _SiteLogsScreenState extends State<SiteLogsScreen> {
|
||||||
padding: padding,
|
padding: padding,
|
||||||
icon: Icon(context.platformIcons.share, size: 30),
|
icon: Icon(context.platformIcons.share, size: 30),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
FlutterShare.shareFile(title: '${widget.site.name} logs', filePath: widget.site.logFile);
|
Share.shareFile(title: '${widget.site.name} logs', filePath: widget.site.logFile, filename: '${widget.site.name}.log');
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
|
@ -172,7 +172,7 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
|
||||||
try {
|
try {
|
||||||
var config = await widget.site.renderConfig();
|
var config = await widget.site.renderConfig();
|
||||||
Utils.openPage(context, (context) {
|
Utils.openPage(context, (context) {
|
||||||
return RenderedConfigScreen(config: config);
|
return RenderedConfigScreen(config: config, name: widget.site.name);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Utils.popError(context, 'Failed to render the site config', err);
|
Utils.popError(context, 'Failed to render the site config', err);
|
||||||
|
|
|
@ -6,7 +6,6 @@ import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_share/flutter_share.dart';
|
|
||||||
import 'package:mobile_nebula/components/FormPage.dart';
|
import 'package:mobile_nebula/components/FormPage.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigButtonItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigButtonItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
||||||
|
@ -15,6 +14,7 @@ import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigTextItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigTextItem.dart';
|
||||||
import 'package:mobile_nebula/models/Certificate.dart';
|
import 'package:mobile_nebula/models/Certificate.dart';
|
||||||
import 'package:mobile_nebula/screens/siteConfig/CertificateDetailsScreen.dart';
|
import 'package:mobile_nebula/screens/siteConfig/CertificateDetailsScreen.dart';
|
||||||
|
import 'package:mobile_nebula/services/share.dart';
|
||||||
import 'package:mobile_nebula/services/utils.dart';
|
import 'package:mobile_nebula/services/utils.dart';
|
||||||
|
|
||||||
class CertificateResult {
|
class CertificateResult {
|
||||||
|
@ -140,7 +140,7 @@ class _CertificateScreenState extends State<CertificateScreen> {
|
||||||
ConfigButtonItem(
|
ConfigButtonItem(
|
||||||
content: Text('Share Public Key'),
|
content: Text('Share Public Key'),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await FlutterShare.share(title: 'Please sign and return a certificate', text: pubKey);
|
await Share.share(title: 'Please sign and return a certificate', text: pubKey, filename: 'device.pub');
|
||||||
setState(() {
|
setState(() {
|
||||||
shared = true;
|
shared = true;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,17 +1,27 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||||
import 'package:mobile_nebula/components/SimplePage.dart';
|
import 'package:mobile_nebula/components/SimplePage.dart';
|
||||||
|
import 'package:mobile_nebula/services/share.dart';
|
||||||
|
|
||||||
class RenderedConfigScreen extends StatelessWidget {
|
class RenderedConfigScreen extends StatelessWidget {
|
||||||
final String config;
|
final String config;
|
||||||
|
final String name;
|
||||||
|
|
||||||
RenderedConfigScreen({Key key, this.config}) : super(key: key);
|
RenderedConfigScreen({Key key, this.config, this.name}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SimplePage(
|
return SimplePage(
|
||||||
title: 'Rendered Site Config',
|
title: 'Rendered Site Config',
|
||||||
scrollable: SimpleScrollable.both,
|
scrollable: SimpleScrollable.both,
|
||||||
|
trailingActions: <Widget>[
|
||||||
|
PlatformIconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: Icon(context.platformIcons.share, size: 28.0),
|
||||||
|
onPressed: () => Share.share(title: '$name.yaml', text: config, filename: '$name.yaml'),
|
||||||
|
)
|
||||||
|
],
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.all(5),
|
padding: EdgeInsets.all(5),
|
||||||
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
// This code comes from https://github.com/lubritto/flutter_share with bugfixes for ipad
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class Share {
|
||||||
|
static const _channel = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
|
||||||
|
|
||||||
|
/// Shares a string of text
|
||||||
|
/// - title: Title of message or subject if sending an email
|
||||||
|
/// - text: The text to share
|
||||||
|
/// - filename: The filename to use if sending over airdrop for example
|
||||||
|
static Future<bool> share({@required String title, @required String text, @required String filename}) async {
|
||||||
|
assert(title != null && title.isNotEmpty);
|
||||||
|
assert(text != null && text.isNotEmpty);
|
||||||
|
assert(filename != null && filename.isNotEmpty);
|
||||||
|
|
||||||
|
if (title == null || title.isEmpty) {
|
||||||
|
throw FlutterError('Title cannot be null');
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool success = await _channel.invokeMethod('share', <String, dynamic>{
|
||||||
|
'title': title,
|
||||||
|
'text': text,
|
||||||
|
'filename': filename,
|
||||||
|
});
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shares a local file
|
||||||
|
/// - title: Title of message or subject if sending an email
|
||||||
|
/// - filePath: Path to the file to share
|
||||||
|
/// - filename: An optional filename to override the existing file
|
||||||
|
static Future<bool> shareFile({@required String title, @required String filePath, String filename}) async {
|
||||||
|
assert(title != null && title.isNotEmpty);
|
||||||
|
assert(filePath != null && filePath.isNotEmpty);
|
||||||
|
|
||||||
|
if (title == null || title.isEmpty) {
|
||||||
|
throw FlutterError('Title cannot be null');
|
||||||
|
} else if (filePath == null || filePath.isEmpty) {
|
||||||
|
throw FlutterError('FilePath cannot be null');
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool success =
|
||||||
|
await _channel.invokeMethod('shareFile', <String, dynamic>{
|
||||||
|
'title': title,
|
||||||
|
'filePath': filePath,
|
||||||
|
'filename': filename,
|
||||||
|
});
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
}
|
|
@ -111,13 +111,6 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
flutter_share:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_share
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2+1"
|
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
|
@ -11,7 +11,7 @@ description: Mobile Nebula Client
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.0.0+31
|
version: 0.0.32+4
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.1.0 <3.0.0"
|
sdk: ">=2.1.0 <3.0.0"
|
||||||
|
@ -27,7 +27,6 @@ dependencies:
|
||||||
path_provider: ^1.6.0
|
path_provider: ^1.6.0
|
||||||
file_picker: ^1.9.0
|
file_picker: ^1.9.0
|
||||||
barcode_scan: ^3.0.1
|
barcode_scan: ^3.0.1
|
||||||
flutter_share: ^1.0.2
|
|
||||||
uuid: ^2.0.4
|
uuid: ^2.0.4
|
||||||
package_info: '>=0.4.1 <2.0.0'
|
package_info: '>=0.4.1 <2.0.0'
|
||||||
url_launcher: ^5.4.10
|
url_launcher: ^5.4.10
|
||||||
|
|
Loading…
Reference in New Issue