mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-06-06 13:53:54 +02:00
feat: Add geolocation and haptics plugins (#1599)
* init geolocation plugin * ios impl (w/o js api) * generate ts api * use newer tauri commit * add temporary postinstall * include src in files * guest-js * just ship dist-js for now * fix watcher * fix android compile error * fix android build for real * fix heading type * initial getCurrentPosition android impl (wip) * prevent panics if errors (strings) are sent over the channel * Add android watchPosition implementation * init haptics plugin (android) * ios and new apis (ANDROID IS LIKELY BROKEN - MAY NOT EVEN COMPILE) * use tauri-specta that accounts for raw fn arg idents * add complete android support (it's not working great due to random soft-/hardware support) * fix(haptics): Fix the NotificationFeedbackType::Success and Version (#1) * Fix success feedback and version * Apply suggestions from code review * Update package.json --------- Co-authored-by: Fabian-Lars <118197967+FabianLars-crabnebula@users.noreply.github.com> * android: improve permission callback handling * keep track of ongoing perms requests * rebuild * license headers * rm sqlite feat * fmt * what diff u talkin bout? * ignore dist-js again * fix audits * dedupe api.js * clippy * changefiles * readmes * clean up todos * rm dsstore * rm wrong feats * mirror * covector * rebuild * ios requires the wry feature * lint * update lock --------- Co-authored-by: fabianlars <fabianlars@fabianlars.de> Co-authored-by: Brendan Allan <brendonovich@outlook.com> Co-authored-by: Naman Garg <155433377+naman-crabnebula@users.noreply.github.com> Co-authored-by: Lucas Nogueira <lucas@crabnebula.dev>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
Package.resolved
|
||||
@@ -0,0 +1,33 @@
|
||||
// swift-tools-version:5.3
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "tauri-plugin-geolocation",
|
||||
platforms: [
|
||||
.iOS(.v13),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "tauri-plugin-geolocation",
|
||||
type: .static,
|
||||
targets: ["tauri-plugin-geolocation"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "Tauri", path: "../.tauri/tauri-api")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "tauri-plugin-geolocation",
|
||||
dependencies: [
|
||||
.byName(name: "Tauri")
|
||||
],
|
||||
path: "Sources")
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tauri Plugin Geolocation
|
||||
|
||||
A description of this package.
|
||||
@@ -0,0 +1,246 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import SwiftRs
|
||||
import Tauri
|
||||
import UIKit
|
||||
import WebKit
|
||||
import CoreLocation
|
||||
|
||||
class GetPositionArgs: Decodable {
|
||||
let enableHighAccuracy: Bool?
|
||||
}
|
||||
|
||||
class WatchPositionArgs: Decodable {
|
||||
let options: GetPositionArgs
|
||||
let channel: Channel
|
||||
}
|
||||
|
||||
class ClearWatchArgs: Decodable {
|
||||
let channelId: UInt32
|
||||
}
|
||||
|
||||
class GeolocationPlugin: Plugin, CLLocationManagerDelegate {
|
||||
private let locationManager = CLLocationManager()
|
||||
private var isUpdatingLocation: Bool = false
|
||||
private var permissionRequests: [Invoke] = []
|
||||
private var positionRequests: [Invoke] = []
|
||||
private var watcherChannels: [Channel] = []
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
locationManager.delegate = self
|
||||
}
|
||||
|
||||
//
|
||||
// Tauri commands
|
||||
//
|
||||
|
||||
@objc public func getCurrentPosition(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(GetPositionArgs.self)
|
||||
|
||||
self.positionRequests.append(invoke)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if args.enableHighAccuracy == true {
|
||||
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
} else {
|
||||
self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
|
||||
}
|
||||
|
||||
// TODO: Use the authorizationStatus instance property with locationManagerDidChangeAuthorization(_:) instead.
|
||||
if CLLocationManager.authorizationStatus() == .notDetermined {
|
||||
self.locationManager.requestWhenInUseAuthorization()
|
||||
} else {
|
||||
self.locationManager.requestLocation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func watchPosition(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(WatchPositionArgs.self)
|
||||
|
||||
self.watcherChannels.append(args.channel)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if args.options.enableHighAccuracy == true {
|
||||
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
} else {
|
||||
self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
|
||||
}
|
||||
|
||||
// TODO: Use the authorizationStatus instance property with locationManagerDidChangeAuthorization(_:) instead.
|
||||
if CLLocationManager.authorizationStatus() == .notDetermined {
|
||||
self.locationManager.requestWhenInUseAuthorization()
|
||||
} else {
|
||||
self.locationManager.startUpdatingLocation()
|
||||
self.isUpdatingLocation = true
|
||||
}
|
||||
}
|
||||
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@objc public func clearWatch(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(ClearWatchArgs.self)
|
||||
|
||||
self.watcherChannels = self.watcherChannels.filter { $0.id != args.channelId }
|
||||
|
||||
// TODO: capacitor plugin calls stopUpdating unconditionally
|
||||
if self.watcherChannels.isEmpty {
|
||||
self.stopUpdating()
|
||||
}
|
||||
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@objc override public func checkPermissions(_ invoke: Invoke) {
|
||||
var status: String = ""
|
||||
|
||||
if CLLocationManager.locationServicesEnabled() {
|
||||
// TODO: Use the authorizationStatus instance property with locationManagerDidChangeAuthorization(_:) instead.
|
||||
switch CLLocationManager.authorizationStatus() {
|
||||
case .notDetermined:
|
||||
status = "prompt"
|
||||
case .restricted, .denied:
|
||||
status = "denied"
|
||||
case .authorizedAlways, .authorizedWhenInUse:
|
||||
status = "granted"
|
||||
@unknown default:
|
||||
status = "prompt"
|
||||
}
|
||||
} else {
|
||||
invoke.reject("Location services are not enabled.")
|
||||
return
|
||||
}
|
||||
|
||||
let result = ["location": status, "coarseLocation": status]
|
||||
|
||||
invoke.resolve(result)
|
||||
}
|
||||
|
||||
@objc override public func requestPermissions(_ invoke: Invoke) {
|
||||
if CLLocationManager.locationServicesEnabled() {
|
||||
// TODO: Use the authorizationStatus instance property with locationManagerDidChangeAuthorization(_:) instead.
|
||||
if CLLocationManager.authorizationStatus() == .notDetermined {
|
||||
self.permissionRequests.append(invoke)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.locationManager.requestWhenInUseAuthorization()
|
||||
}
|
||||
} else {
|
||||
checkPermissions(invoke)
|
||||
}
|
||||
} else {
|
||||
invoke.reject("Location services are not enabled.")
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Delegate methods
|
||||
//
|
||||
|
||||
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
Logger.error(error)
|
||||
|
||||
let requests = self.positionRequests + self.permissionRequests
|
||||
self.positionRequests.removeAll()
|
||||
self.permissionRequests.removeAll()
|
||||
|
||||
for request in requests {
|
||||
request.reject(error.localizedDescription)
|
||||
}
|
||||
|
||||
for channel in self.watcherChannels {
|
||||
do {
|
||||
try channel.send(error.localizedDescription)
|
||||
} catch {
|
||||
Logger.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
// Respond to all getCurrentPosition() calls.
|
||||
for request in self.positionRequests {
|
||||
// The capacitor plugin uses locations.first but .last should be the most current one
|
||||
// and i don't see a reason to use old locations
|
||||
if let location = locations.last {
|
||||
let result = convertLocation(location)
|
||||
request.resolve(result)
|
||||
} else {
|
||||
request.reject("Location service returned an empty Location array.")
|
||||
}
|
||||
}
|
||||
|
||||
for channel in self.watcherChannels {
|
||||
// The capacitor plugin uses locations.first but .last should be the most recent one
|
||||
// and i don't see a reason to use old locations
|
||||
if let location = locations.last {
|
||||
let result = convertLocation(location)
|
||||
do {
|
||||
try channel.send(result)
|
||||
} catch {
|
||||
Logger.error(error)
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
try channel.send("Location service returned an empty Location array.")
|
||||
} catch {
|
||||
Logger.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
|
||||
let requests = self.permissionRequests
|
||||
self.permissionRequests.removeAll()
|
||||
|
||||
for request in requests {
|
||||
checkPermissions(request)
|
||||
}
|
||||
|
||||
if !self.positionRequests.isEmpty {
|
||||
self.locationManager.requestLocation()
|
||||
}
|
||||
|
||||
if !self.watcherChannels.isEmpty && !self.isUpdatingLocation {
|
||||
self.locationManager.startUpdatingLocation()
|
||||
self.isUpdatingLocation = true
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Internal/Helper methods
|
||||
//
|
||||
|
||||
// TODO: Why is this pub in capacitor
|
||||
private func stopUpdating() {
|
||||
self.locationManager.stopUpdatingLocation()
|
||||
self.isUpdatingLocation = false
|
||||
}
|
||||
|
||||
private func convertLocation(_ location: CLLocation) -> JsonObject {
|
||||
var ret: JsonObject = [:]
|
||||
var coords: JsonObject = [:]
|
||||
|
||||
coords["latitude"] = location.coordinate.latitude
|
||||
coords["longitude"] = location.coordinate.longitude
|
||||
coords["accuracy"] = location.horizontalAccuracy
|
||||
coords["altitude"] = location.altitude
|
||||
coords["altitudeAccuracy"] = location.verticalAccuracy
|
||||
coords["speed"] = location.speed
|
||||
coords["heading"] = location.course
|
||||
ret["timestamp"] = Int((location.timestamp.timeIntervalSince1970 * 1000))
|
||||
ret["coords"] = coords
|
||||
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_geolocation")
|
||||
func initPlugin() -> Plugin {
|
||||
return GeolocationPlugin()
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import XCTest
|
||||
@testable import ExamplePlugin
|
||||
|
||||
final class ExamplePluginTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
let plugin = ExamplePlugin()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user