From 7e0b9a63306fe1b0b2515b48759d7d6953c63af3 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 22 Apr 2026 22:42:53 +0300 Subject: [PATCH] files --- CHANGELOG.md | 24 ++++ android/src/main/AndroidManifest.xml | 12 ++ .../main/kotlin/io/swipelab/ux/FilePlugin.kt | 131 ++++++++++++++++++ .../main/kotlin/io/swipelab/ux/UxPlugin.kt | 1 + android/src/main/res/xml/ux_file_paths.xml | 5 + ios/Classes/FilePlugin.swift | 115 +++++++++++++++ ios/Classes/KeyboardPlugin.swift | 18 +-- ios/Classes/NativePlugin.swift | 18 +++ ios/Classes/UxPlugin.swift | 1 + ios/ux.podspec | 4 +- lib/src/file.dart | 70 ++++++++++ lib/ux.dart | 1 + macos/Classes/FilePlugin.swift | 117 ++++++++++++++++ macos/Classes/NativePlugin.swift | 18 +++ macos/Classes/UxPlugin.swift | 19 +++ macos/ux.podspec | 13 ++ pubspec.yaml | 4 +- 17 files changed, 552 insertions(+), 19 deletions(-) create mode 100644 android/src/main/kotlin/io/swipelab/ux/FilePlugin.kt create mode 100644 android/src/main/res/xml/ux_file_paths.xml create mode 100644 ios/Classes/FilePlugin.swift create mode 100644 lib/src/file.dart create mode 100644 macos/Classes/FilePlugin.swift create mode 100644 macos/Classes/NativePlugin.swift create mode 100644 macos/Classes/UxPlugin.swift create mode 100644 macos/ux.podspec 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