diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6549d0a..8be479a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,27 @@
+### 0.6.0
+- `UxFile`: new module for handing files to the OS
+ - `UxFile.share(path, {title, mimeType, sourceRect})` — iOS `UIActivityViewController`,
+ macOS `NSSharingServicePicker`, Android `Intent.ACTION_SEND` via FileProvider.
+ `sourceRect` anchors the popover on iPad / macOS.
+ - `UxFile.open(path, {mimeType})` — in-app preview where possible:
+ iOS `QLPreviewController`, macOS `QLPreviewPanel` (preview on top of the app,
+ no foreground loss), Android `Intent.ACTION_VIEW` with MIME inference from
+ `MimeTypeMap` + a fallback set of text/code extensions so unknown types
+ still resolve to `text/plain` instead of `application/octet-stream`.
+- **macOS**: new platform. Plugin registry + `FilePlugin`; keyboard and sensor
+ plugins remain iOS-only.
+- Android: ships a FileProvider under `${applicationId}.ux.fileprovider`
+ scoped to `ux_share/` in the app cache — host apps don't need manifest
+ plumbing.
+- iOS: `UxWindow` helper (`keyWindow` / `topViewController`) in
+ `NativePlugin.swift`, shared between keyboard and file plugins.
+
+### 0.5.0
+- `UxSensor.orientation`: accelerometer-driven physical device rotation,
+ independent of the OS auto-rotate setting or UI orientation lock
+- `AppInfo`: version + buildNumber surface
+- `UxKeyboard`: focus tracking integration
+
### 0.4.0
- `package:ux/testing.dart`: new entry point for test-only utilities
- `matchesTextGolden(path, {update})`: `matchesGoldenFile`-style matcher for
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 2f68a6e..be27b0b 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,3 +1,15 @@
+
+
+
+
+
+
diff --git a/android/src/main/kotlin/io/swipelab/ux/FilePlugin.kt b/android/src/main/kotlin/io/swipelab/ux/FilePlugin.kt
new file mode 100644
index 0000000..1878af7
--- /dev/null
+++ b/android/src/main/kotlin/io/swipelab/ux/FilePlugin.kt
@@ -0,0 +1,131 @@
+package io.swipelab.ux
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.webkit.MimeTypeMap
+import androidx.core.content.FileProvider
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import java.io.File
+
+class FilePlugin : NativePlugin, MethodChannel.MethodCallHandler {
+ private var methodChannel: MethodChannel? = null
+ private var context: Context? = null
+ private var activity: Activity? = null
+
+ override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
+ context = binding.applicationContext
+ methodChannel = MethodChannel(binding.binaryMessenger, "ux/file").also {
+ it.setMethodCallHandler(this)
+ }
+ }
+
+ override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
+ methodChannel?.setMethodCallHandler(null)
+ methodChannel = null
+ context = null
+ }
+
+ override fun onAttachedToActivity(binding: ActivityPluginBinding) {
+ activity = binding.activity
+ }
+
+ override fun onDetachedFromActivity() {
+ activity = null
+ }
+
+ override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
+ when (call.method) {
+ "share" -> handleShare(call, result)
+ "open" -> handleOpen(call, result)
+ else -> result.notImplemented()
+ }
+ }
+
+ private fun handleShare(call: MethodCall, result: MethodChannel.Result) {
+ val ctx = context ?: return result.error("no_context", "no application context", null)
+ val act = activity ?: return result.error("no_activity", "plugin not attached to an activity", null)
+ val path = call.argument("path")
+ ?: return result.error("bad_args", "path is required", null)
+ val title = call.argument("title")
+ val mimeType = call.argument("mimeType")
+
+ val authority = "${ctx.packageName}.ux.fileprovider"
+ val uri = try {
+ FileProvider.getUriForFile(ctx, authority, File(path))
+ } catch (e: IllegalArgumentException) {
+ return result.error("bad_path", "file is outside FileProvider paths: ${e.message}", null)
+ }
+
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ type = mimeType ?: "*/*"
+ putExtra(Intent.EXTRA_STREAM, uri)
+ title?.let { putExtra(Intent.EXTRA_SUBJECT, it) }
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+
+ val chooser = Intent.createChooser(intent, title).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ act.startActivity(chooser)
+ result.success(true)
+ }
+
+ private fun handleOpen(call: MethodCall, result: MethodChannel.Result) {
+ val ctx = context ?: return result.error("no_context", "no application context", null)
+ val act = activity ?: return result.error("no_activity", "plugin not attached to an activity", null)
+ val path = call.argument("path")
+ ?: return result.error("bad_args", "path is required", null)
+ val mimeType = inferMime(call.argument("mimeType"), path)
+
+ val authority = "${ctx.packageName}.ux.fileprovider"
+ val uri = try {
+ FileProvider.getUriForFile(ctx, authority, File(path))
+ } catch (e: IllegalArgumentException) {
+ return result.error("bad_path", "file is outside FileProvider paths: ${e.message}", null)
+ }
+
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ setDataAndType(uri, mimeType)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ try {
+ act.startActivity(intent)
+ result.success(true)
+ } catch (_: ActivityNotFoundException) {
+ result.success(false)
+ }
+ }
+
+ private fun inferMime(supplied: String?, path: String): String {
+ if (!supplied.isNullOrBlank() && supplied != "application/octet-stream") {
+ return supplied
+ }
+ val dot = path.lastIndexOf('.')
+ val ext = if (dot in 0 until path.length - 1) path.substring(dot + 1).lowercase() else ""
+ if (ext.isNotEmpty()) {
+ val guessed = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
+ if (!guessed.isNullOrBlank()) return guessed
+ if (ext in textExtensions) return "text/plain"
+ }
+ return supplied ?: "*/*"
+ }
+
+ companion object {
+ private val textExtensions = setOf(
+ "dart", "swift", "kt", "kts", "java", "scala", "groovy",
+ "py", "rb", "php", "pl", "sh", "bash", "zsh", "fish",
+ "ts", "tsx", "jsx", "mjs", "cjs",
+ "go", "rs", "c", "h", "cpp", "hpp", "cc", "hh", "m", "mm",
+ "lua", "tcl", "r", "ex", "exs", "elm", "hs", "clj", "cljs", "scm",
+ "proto", "thrift", "sql",
+ "yml", "yaml", "toml", "ini", "conf", "cfg", "env", "properties",
+ "gradle", "lock", "diff", "patch",
+ "md", "markdown", "log", "txt",
+ )
+ }
+}
diff --git a/android/src/main/kotlin/io/swipelab/ux/UxPlugin.kt b/android/src/main/kotlin/io/swipelab/ux/UxPlugin.kt
index 3200cf2..494c766 100644
--- a/android/src/main/kotlin/io/swipelab/ux/UxPlugin.kt
+++ b/android/src/main/kotlin/io/swipelab/ux/UxPlugin.kt
@@ -8,6 +8,7 @@ class UxPlugin : FlutterPlugin, ActivityAware {
private val plugins: List = listOf(
KeyboardPlugin(),
SensorPlugin(),
+ FilePlugin(),
)
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) =
diff --git a/android/src/main/res/xml/ux_file_paths.xml b/android/src/main/res/xml/ux_file_paths.xml
new file mode 100644
index 0000000..c8fa002
--- /dev/null
+++ b/android/src/main/res/xml/ux_file_paths.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/ios/Classes/FilePlugin.swift b/ios/Classes/FilePlugin.swift
new file mode 100644
index 0000000..b1a5445
--- /dev/null
+++ b/ios/Classes/FilePlugin.swift
@@ -0,0 +1,115 @@
+import Flutter
+import QuickLook
+import UIKit
+
+public class FilePlugin: NSObject, NativePlugin {
+ private var channel: FlutterMethodChannel?
+ private var previewDataSource: FilePreviewDataSource?
+
+ public func register(with registrar: FlutterPluginRegistrar) {
+ let c = FlutterMethodChannel(name: "ux/file", binaryMessenger: registrar.messenger())
+ c.setMethodCallHandler { [weak self] call, result in
+ self?.handle(call, result: result)
+ }
+ channel = c
+ }
+
+ private func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ switch call.method {
+ case "share": handleShare(call, result: result)
+ case "open": handleOpen(call, result: result)
+ default: result(FlutterMethodNotImplemented)
+ }
+ }
+
+ private func handleShare(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ guard let args = call.arguments as? [String: Any],
+ let path = args["path"] as? String else {
+ return result(FlutterError(code: "bad_args", message: "path is required", details: nil))
+ }
+ guard let topVC = UxWindow.topViewController else {
+ return result(FlutterError(code: "no_view", message: "no top view controller", details: nil))
+ }
+
+ let url = URL(fileURLWithPath: path)
+ let title = args["title"] as? String
+
+ let items: [Any] = title.map { [FileActivityItemSource(url: url, title: $0)] } ?? [url]
+ let vc = UIActivityViewController(activityItems: items, applicationActivities: nil)
+
+ if UIDevice.current.userInterfaceIdiom == .pad,
+ let popover = vc.popoverPresentationController {
+ popover.sourceView = topVC.view
+ if let r = args["sourceRect"] as? [String: Any],
+ let x = (r["x"] as? NSNumber)?.doubleValue,
+ let y = (r["y"] as? NSNumber)?.doubleValue,
+ let w = (r["w"] as? NSNumber)?.doubleValue,
+ let h = (r["h"] as? NSNumber)?.doubleValue {
+ popover.sourceRect = CGRect(x: x, y: y, width: w, height: h)
+ } else {
+ let b = topVC.view.bounds
+ popover.sourceRect = CGRect(x: b.midX, y: b.midY, width: 1, height: 1)
+ popover.permittedArrowDirections = []
+ }
+ }
+
+ topVC.present(vc, animated: true) {
+ result(true)
+ }
+ }
+
+ private func handleOpen(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ guard let args = call.arguments as? [String: Any],
+ let path = args["path"] as? String else {
+ return result(FlutterError(code: "bad_args", message: "path is required", details: nil))
+ }
+ guard let topVC = UxWindow.topViewController else {
+ return result(FlutterError(code: "no_view", message: "no top view controller", details: nil))
+ }
+
+ let url = URL(fileURLWithPath: path)
+ let ds = FilePreviewDataSource(url: url)
+ // QLPreviewController requires a strong-retained data source.
+ previewDataSource = ds
+
+ let vc = QLPreviewController()
+ vc.dataSource = ds
+
+ topVC.present(vc, animated: true) {
+ result(true)
+ }
+ }
+}
+
+private final class FilePreviewDataSource: NSObject, QLPreviewControllerDataSource {
+ let url: URL
+ init(url: URL) { self.url = url }
+
+ func numberOfPreviewItems(in controller: QLPreviewController) -> Int { 1 }
+
+ func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
+ url as QLPreviewItem
+ }
+}
+
+private final class FileActivityItemSource: NSObject, UIActivityItemSource {
+ let url: URL
+ let title: String
+
+ init(url: URL, title: String) {
+ self.url = url
+ self.title = title
+ }
+
+ func activityViewControllerPlaceholderItem(_ controller: UIActivityViewController) -> Any {
+ url
+ }
+
+ func activityViewController(_ controller: UIActivityViewController, itemForActivityType type: UIActivity.ActivityType?) -> Any? {
+ url
+ }
+
+ func activityViewController(_ controller: UIActivityViewController, subjectForActivityType type: UIActivity.ActivityType?) -> String {
+ title
+ }
+}
diff --git a/ios/Classes/KeyboardPlugin.swift b/ios/Classes/KeyboardPlugin.swift
index 0e5dbcf..abf1950 100644
--- a/ios/Classes/KeyboardPlugin.swift
+++ b/ios/Classes/KeyboardPlugin.swift
@@ -267,22 +267,8 @@ public class KeyboardPlugin: NSObject, NativePlugin {
}
private func setupPanGesture() {
- var targetView: UIView?
-
- if let window = UIApplication.shared.delegate?.window ?? nil {
- targetView = window.rootViewController?.view ?? window
- } else {
- for scene in UIApplication.shared.connectedScenes {
- if let ws = scene as? UIWindowScene {
- for window in ws.windows where window.isKeyWindow {
- targetView = window.rootViewController?.view ?? window
- break
- }
- }
- }
- }
-
- guard let view = targetView else { return }
+ guard let window = UxWindow.keyWindow else { return }
+ let view: UIView = window.rootViewController?.view ?? window
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
pan.cancelsTouchesInView = false
diff --git a/ios/Classes/NativePlugin.swift b/ios/Classes/NativePlugin.swift
index 3441162..35f8f18 100644
--- a/ios/Classes/NativePlugin.swift
+++ b/ios/Classes/NativePlugin.swift
@@ -1,5 +1,23 @@
import Flutter
+import UIKit
public protocol NativePlugin {
func register(with registrar: FlutterPluginRegistrar)
}
+
+public enum UxWindow {
+ public static var keyWindow: UIWindow? {
+ if let w = UIApplication.shared.delegate?.window ?? nil { return w }
+ for scene in UIApplication.shared.connectedScenes {
+ guard let ws = scene as? UIWindowScene else { continue }
+ for window in ws.windows where window.isKeyWindow { return window }
+ }
+ return nil
+ }
+
+ public static var topViewController: UIViewController? {
+ var vc = keyWindow?.rootViewController
+ while let presented = vc?.presentedViewController { vc = presented }
+ return vc
+ }
+}
diff --git a/ios/Classes/UxPlugin.swift b/ios/Classes/UxPlugin.swift
index 239d9f6..d20d564 100644
--- a/ios/Classes/UxPlugin.swift
+++ b/ios/Classes/UxPlugin.swift
@@ -8,6 +8,7 @@ public class UxPlugin: NSObject, FlutterPlugin {
plugins = [
KeyboardPlugin(),
SensorPlugin(),
+ FilePlugin(),
]
for plugin in plugins {
plugin.register(with: registrar)
diff --git a/ios/ux.podspec b/ios/ux.podspec
index 96f0e05..ee9294f 100644
--- a/ios/ux.podspec
+++ b/ios/ux.podspec
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = 'ux'
- s.version = '0.2.0'
- s.summary = 'UX Kit – Flutter plugin with keyboard tracking and interactive dismiss.'
+ s.version = '0.6.0'
+ s.summary = 'UX Kit — Flutter plugin: keyboard, sensor, and file share/open.'
s.homepage = 'https://swipelab.co/ux.html'
s.license = { :file => '../LICENSE' }
s.author = { 'Swipelab' => 'hello@swipelab.co' }
diff --git a/lib/src/file.dart b/lib/src/file.dart
new file mode 100644
index 0000000..1da23be
--- /dev/null
+++ b/lib/src/file.dart
@@ -0,0 +1,70 @@
+import 'dart:ui' show Rect;
+
+import 'package:flutter/services.dart';
+
+class UxFile {
+ UxFile._();
+
+ static const _channel = MethodChannel('ux/file');
+
+ /// Present the native share sheet for a file on disk.
+ ///
+ /// [sourceRect] is the originating widget's rect in Flutter logical
+ /// pixels (global coordinates). Required on iPad and macOS (anchor for
+ /// the popover); ignored on iPhone and Android. Pass null on iPad/macOS
+ /// to center the popover — typical fallback when the originating widget
+ /// has been disposed by the time bytes are ready.
+ ///
+ /// [mimeType] hints `Intent.setType` on Android. Ignored on Apple
+ /// platforms (the share sheet infers from the file extension).
+ ///
+ /// [title] populates the subject hint (Mail subject on iOS/macOS,
+ /// chooser title on Android).
+ ///
+ /// Returns true if the sheet was presented. Returns false if the host
+ /// couldn't present it (no activity on Android, no window on macOS).
+ static Future share({
+ required String path,
+ String? title,
+ String? mimeType,
+ Rect? sourceRect,
+ }) async {
+ final result = await _channel.invokeMethod('share', {
+ 'path': path,
+ if (title != null) 'title': title,
+ if (mimeType != null) 'mimeType': mimeType,
+ if (sourceRect != null)
+ 'sourceRect': {
+ 'x': sourceRect.left,
+ 'y': sourceRect.top,
+ 'w': sourceRect.width,
+ 'h': sourceRect.height,
+ },
+ });
+ return result ?? false;
+ }
+
+ /// Open a file on disk with the system's default viewer.
+ ///
+ /// - iOS: Quick Look preview (`QLPreviewController`) — modal with native
+ /// preview for common types. The built-in toolbar still exposes Share.
+ /// - macOS: hands off to the system default app (`NSWorkspace.open`).
+ /// - Android: `Intent.ACTION_VIEW` — launches the default viewer app.
+ ///
+ /// [mimeType] hints the viewer on Android. Ignored on Apple platforms
+ /// (the framework infers from the file extension).
+ ///
+ /// Returns true if the viewer opened. Returns false when no viewer is
+ /// available (Android: no app registered for the MIME; macOS: no
+ /// associated app).
+ static Future open({
+ required String path,
+ String? mimeType,
+ }) async {
+ final result = await _channel.invokeMethod('open', {
+ 'path': path,
+ if (mimeType != null) 'mimeType': mimeType,
+ });
+ return result ?? false;
+ }
+}
diff --git a/lib/ux.dart b/lib/ux.dart
index 87f6463..cef6cb4 100644
--- a/lib/ux.dart
+++ b/lib/ux.dart
@@ -8,5 +8,6 @@ export 'src/app_info.dart';
export 'src/bend_box.dart';
export 'src/json_extension.dart';
export 'src/bezier.dart';
+export 'src/file.dart';
export 'src/keyboard.dart';
export 'src/sensor.dart';
diff --git a/macos/Classes/FilePlugin.swift b/macos/Classes/FilePlugin.swift
new file mode 100644
index 0000000..3b6d756
--- /dev/null
+++ b/macos/Classes/FilePlugin.swift
@@ -0,0 +1,117 @@
+import FlutterMacOS
+import AppKit
+import Quartz
+
+public class FilePlugin: NSObject, NativePlugin {
+ private var channel: FlutterMethodChannel?
+
+ public func register(with registrar: FlutterPluginRegistrar) {
+ let c = FlutterMethodChannel(name: "ux/file", binaryMessenger: registrar.messenger)
+ c.setMethodCallHandler { [weak self] call, result in
+ self?.handle(call, result: result)
+ }
+ channel = c
+ }
+
+ private func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ switch call.method {
+ case "share": handleShare(call, result: result)
+ case "open": handleOpen(call, result: result)
+ default: result(FlutterMethodNotImplemented)
+ }
+ }
+
+ private func handleShare(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ guard let args = call.arguments as? [String: Any],
+ let path = args["path"] as? String else {
+ return result(FlutterError(code: "bad_args", message: "path is required", details: nil))
+ }
+ guard let view = UxWindow.flutterView else {
+ return result(FlutterError(code: "no_view", message: "no Flutter view", details: nil))
+ }
+
+ let url = URL(fileURLWithPath: path)
+ let picker = NSSharingServicePicker(items: [url])
+
+ let rect: NSRect
+ if let r = args["sourceRect"] as? [String: Any],
+ let x = (r["x"] as? NSNumber)?.doubleValue,
+ let y = (r["y"] as? NSNumber)?.doubleValue,
+ let w = (r["w"] as? NSNumber)?.doubleValue,
+ let h = (r["h"] as? NSNumber)?.doubleValue {
+ if view.isFlipped {
+ rect = NSRect(x: x, y: y, width: w, height: h)
+ } else {
+ rect = NSRect(x: x, y: view.bounds.height - y - h, width: w, height: h)
+ }
+ } else {
+ rect = NSRect(x: view.bounds.midX, y: view.bounds.midY, width: 1, height: 1)
+ }
+
+ picker.show(relativeTo: rect, of: view, preferredEdge: .minY)
+ result(true)
+ }
+
+ private func handleOpen(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ guard let args = call.arguments as? [String: Any],
+ let path = args["path"] as? String else {
+ return result(FlutterError(code: "bad_args", message: "path is required", details: nil))
+ }
+ let url = URL(fileURLWithPath: path)
+
+ // Prefer in-app Quick Look (keeps Banlu in the foreground).
+ // Fall back to NSWorkspace.open if there's no window to host the panel.
+ if let flutterView = UxWindow.flutterView,
+ let window = flutterView.window,
+ let panel = QLPreviewPanel.shared() {
+ let responder = UxQLPreviewResponder(url: url, window: window)
+ flutterView.addSubview(responder)
+ window.makeFirstResponder(responder)
+ panel.updateController()
+ panel.makeKeyAndOrderFront(nil)
+ result(true)
+ return
+ }
+
+ result(NSWorkspace.shared.open(url))
+ }
+}
+
+private final class UxQLPreviewResponder: NSView, QLPreviewPanelDataSource {
+ let url: URL
+ private weak var previousFirstResponder: NSResponder?
+ private weak var previousWindow: NSWindow?
+
+ init(url: URL, window: NSWindow) {
+ self.url = url
+ self.previousWindow = window
+ self.previousFirstResponder = window.firstResponder
+ super.init(frame: .zero)
+ }
+
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
+
+ override var acceptsFirstResponder: Bool { true }
+
+ override func acceptsPreviewPanelControl(_ panel: QLPreviewPanel) -> Bool { true }
+
+ override func beginPreviewPanelControl(_ panel: QLPreviewPanel) {
+ panel.dataSource = self
+ }
+
+ override func endPreviewPanelControl(_ panel: QLPreviewPanel) {
+ panel.dataSource = nil
+ let win = previousWindow
+ let prev = previousFirstResponder
+ DispatchQueue.main.async { [weak self] in
+ win?.makeFirstResponder(prev)
+ self?.removeFromSuperview()
+ }
+ }
+
+ func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int { 1 }
+
+ func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! {
+ url as QLPreviewItem
+ }
+}
diff --git a/macos/Classes/NativePlugin.swift b/macos/Classes/NativePlugin.swift
new file mode 100644
index 0000000..f638e02
--- /dev/null
+++ b/macos/Classes/NativePlugin.swift
@@ -0,0 +1,18 @@
+import FlutterMacOS
+import AppKit
+
+public protocol NativePlugin {
+ func register(with registrar: FlutterPluginRegistrar)
+}
+
+public enum UxWindow {
+ public static var keyWindow: NSWindow? {
+ NSApp.keyWindow ?? NSApp.mainWindow
+ }
+
+ /// FlutterViewController's view. Used as the anchor for
+ /// `NSSharingServicePicker` popovers.
+ public static var flutterView: NSView? {
+ keyWindow?.contentViewController?.view
+ }
+}
diff --git a/macos/Classes/UxPlugin.swift b/macos/Classes/UxPlugin.swift
new file mode 100644
index 0000000..6394eed
--- /dev/null
+++ b/macos/Classes/UxPlugin.swift
@@ -0,0 +1,19 @@
+import FlutterMacOS
+import AppKit
+
+public class UxPlugin: NSObject, FlutterPlugin {
+ private static var plugins: [NativePlugin] = []
+
+ public static func register(with registrar: FlutterPluginRegistrar) {
+ plugins = [
+ FilePlugin(),
+ ]
+ for plugin in plugins {
+ plugin.register(with: registrar)
+ }
+ }
+
+ public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ result(FlutterMethodNotImplemented)
+ }
+}
diff --git a/macos/ux.podspec b/macos/ux.podspec
new file mode 100644
index 0000000..4df5f94
--- /dev/null
+++ b/macos/ux.podspec
@@ -0,0 +1,13 @@
+Pod::Spec.new do |s|
+ s.name = 'ux'
+ s.version = '0.6.0'
+ s.summary = 'UX Kit — Flutter plugin: file share/open via Quick Look.'
+ s.homepage = 'https://swipelab.co/ux.html'
+ s.license = { :file => '../LICENSE' }
+ s.author = { 'Swipelab' => 'hello@swipelab.co' }
+ s.source = { :path => '.' }
+ s.source_files = 'Classes/**/*.swift'
+ s.dependency 'FlutterMacOS'
+ s.osx.deployment_target = '10.15'
+ s.swift_version = '5.0'
+end
diff --git a/pubspec.yaml b/pubspec.yaml
index b7d8f1a..8c8ee85 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -3,7 +3,7 @@ description: >-
Flutter toolkit for fluid, native-feeling UIs. Includes frame-accurate
keyboard height tracking via FFI with interactive dismiss, bezier utilities,
and layout primitives.
-version: 0.5.0
+version: 0.6.0
homepage: https://swipelab.co/ux.html
repository: https://github.com/swipelab/ux
issue_tracker: https://github.com/swipelab/ux/issues
@@ -35,3 +35,5 @@ flutter:
android:
package: io.swipelab.ux
pluginClass: UxPlugin
+ macos:
+ pluginClass: UxPlugin