@capawesome/capacitor-haptics 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CapawesomeCapacitorHaptics.podspec +17 -0
  2. package/LICENSE +21 -0
  3. package/Package.swift +28 -0
  4. package/README.md +457 -0
  5. package/android/build.gradle +58 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/Haptics.java +284 -0
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/HapticsPlugin.java +239 -0
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/classes/CustomException.java +20 -0
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/classes/CustomExceptions.java +18 -0
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/classes/options/ImpactOptions.java +24 -0
  12. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/classes/options/NotificationOptions.java +24 -0
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/classes/options/PerformAndroidHapticOptions.java +29 -0
  14. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/classes/options/PlayPatternOptions.java +89 -0
  15. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/classes/options/VibrateOptions.java +26 -0
  16. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/classes/results/IsAvailableResult.java +22 -0
  17. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/interfaces/Callback.java +5 -0
  18. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/interfaces/EmptyCallback.java +5 -0
  19. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/interfaces/NonEmptyResultCallback.java +7 -0
  20. package/android/src/main/java/io/capawesome/capacitorjs/plugins/haptics/interfaces/Result.java +7 -0
  21. package/android/src/main/res/.gitkeep +0 -0
  22. package/dist/docs.json +665 -0
  23. package/dist/esm/definitions.d.ts +363 -0
  24. package/dist/esm/definitions.js +150 -0
  25. package/dist/esm/definitions.js.map +1 -0
  26. package/dist/esm/index.d.ts +4 -0
  27. package/dist/esm/index.js +7 -0
  28. package/dist/esm/index.js.map +1 -0
  29. package/dist/esm/web.d.ts +21 -0
  30. package/dist/esm/web.js +100 -0
  31. package/dist/esm/web.js.map +1 -0
  32. package/dist/plugin.cjs.js +263 -0
  33. package/dist/plugin.cjs.js.map +1 -0
  34. package/dist/plugin.js +266 -0
  35. package/dist/plugin.js.map +1 -0
  36. package/ios/Plugin/Classes/Options/ImpactOptions.swift +29 -0
  37. package/ios/Plugin/Classes/Options/NotificationOptions.swift +25 -0
  38. package/ios/Plugin/Classes/Options/PlayPatternOptions.swift +54 -0
  39. package/ios/Plugin/Classes/Results/IsAvailableResult.swift +16 -0
  40. package/ios/Plugin/Enums/CustomError.swift +50 -0
  41. package/ios/Plugin/Haptics.swift +115 -0
  42. package/ios/Plugin/HapticsPlugin.swift +155 -0
  43. package/ios/Plugin/Info.plist +24 -0
  44. package/ios/Plugin/Protocols/Result.swift +5 -0
  45. package/package.json +96 -0
@@ -0,0 +1,54 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc public class PlayPatternOptions: NSObject {
5
+ struct Event {
6
+ let duration: Double?
7
+ let intensity: Float
8
+ let sharpness: Float
9
+ let time: Double
10
+ }
11
+
12
+ let events: [Event]
13
+
14
+ init(_ call: CAPPluginCall) throws {
15
+ self.events = try PlayPatternOptions.getEventsFromCall(call)
16
+ }
17
+
18
+ private static func getEventsFromCall(_ call: CAPPluginCall) throws -> [Event] {
19
+ guard let eventObjects = call.getArray("events", JSObject.self), !eventObjects.isEmpty else {
20
+ throw CustomError.eventsMissing
21
+ }
22
+ var events: [Event] = []
23
+ for eventObject in eventObjects {
24
+ guard let time = (eventObject["time"] as? NSNumber)?.doubleValue else {
25
+ throw CustomError.timeMissing
26
+ }
27
+ guard time >= 0 else {
28
+ throw CustomError.timeInvalid
29
+ }
30
+ guard let intensity = (eventObject["intensity"] as? NSNumber)?.floatValue else {
31
+ throw CustomError.intensityMissing
32
+ }
33
+ guard intensity >= 0, intensity <= 1 else {
34
+ throw CustomError.intensityInvalid
35
+ }
36
+ var sharpness: Float = 0.5
37
+ if let value = (eventObject["sharpness"] as? NSNumber)?.floatValue {
38
+ guard value >= 0, value <= 1 else {
39
+ throw CustomError.sharpnessInvalid
40
+ }
41
+ sharpness = value
42
+ }
43
+ var duration: Double?
44
+ if let value = (eventObject["duration"] as? NSNumber)?.doubleValue {
45
+ guard value > 0 else {
46
+ throw CustomError.durationInvalid
47
+ }
48
+ duration = value
49
+ }
50
+ events.append(Event(duration: duration, intensity: intensity, sharpness: sharpness, time: time))
51
+ }
52
+ return events
53
+ }
54
+ }
@@ -0,0 +1,16 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc public class IsAvailableResult: NSObject, Result {
5
+ let available: Bool
6
+
7
+ init(available: Bool) {
8
+ self.available = available
9
+ }
10
+
11
+ @objc public func toJSObject() -> AnyObject {
12
+ var result = JSObject()
13
+ result["available"] = available
14
+ return result as AnyObject
15
+ }
16
+ }
@@ -0,0 +1,50 @@
1
+ import Foundation
2
+
3
+ enum CustomError: Error {
4
+ case durationInvalid
5
+ case eventsMissing
6
+ case intensityInvalid
7
+ case intensityMissing
8
+ case patternPlaybackFailed
9
+ case sharpnessInvalid
10
+ case styleInvalid
11
+ case timeInvalid
12
+ case timeMissing
13
+ case typeInvalid
14
+
15
+ var code: String? {
16
+ switch self {
17
+ case .patternPlaybackFailed:
18
+ return "PATTERN_PLAYBACK_FAILED"
19
+ default:
20
+ return nil
21
+ }
22
+ }
23
+ }
24
+
25
+ extension CustomError: LocalizedError {
26
+ public var errorDescription: String? {
27
+ switch self {
28
+ case .durationInvalid:
29
+ return NSLocalizedString("duration must be greater than 0.", comment: "durationInvalid")
30
+ case .eventsMissing:
31
+ return NSLocalizedString("events must be provided.", comment: "eventsMissing")
32
+ case .intensityInvalid:
33
+ return NSLocalizedString("intensity must be between 0 and 1.", comment: "intensityInvalid")
34
+ case .intensityMissing:
35
+ return NSLocalizedString("intensity must be provided.", comment: "intensityMissing")
36
+ case .patternPlaybackFailed:
37
+ return NSLocalizedString("The haptic pattern could not be played.", comment: "patternPlaybackFailed")
38
+ case .sharpnessInvalid:
39
+ return NSLocalizedString("sharpness must be between 0 and 1.", comment: "sharpnessInvalid")
40
+ case .styleInvalid:
41
+ return NSLocalizedString("style must be one of the supported values.", comment: "styleInvalid")
42
+ case .timeInvalid:
43
+ return NSLocalizedString("time must be greater than or equal to 0.", comment: "timeInvalid")
44
+ case .timeMissing:
45
+ return NSLocalizedString("time must be provided.", comment: "timeMissing")
46
+ case .typeInvalid:
47
+ return NSLocalizedString("type must be one of the supported values.", comment: "typeInvalid")
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,115 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import AudioToolbox
4
+ import CoreHaptics
5
+ import UIKit
6
+
7
+ @objc public class Haptics: NSObject {
8
+ private var engine: CHHapticEngine?
9
+ private let plugin: HapticsPlugin
10
+ private var selectionFeedbackGenerator: UISelectionFeedbackGenerator?
11
+
12
+ init(plugin: HapticsPlugin) {
13
+ self.plugin = plugin
14
+ }
15
+
16
+ @objc public func impact(_ options: ImpactOptions, completion: @escaping (Error?) -> Void) {
17
+ DispatchQueue.main.async {
18
+ let generator = UIImpactFeedbackGenerator(style: options.style)
19
+ generator.prepare()
20
+ generator.impactOccurred()
21
+ completion(nil)
22
+ }
23
+ }
24
+
25
+ @objc public func isAvailable(completion: @escaping (_ result: IsAvailableResult?, _ error: Error?) -> Void) {
26
+ completion(IsAvailableResult(available: supportsHaptics()), nil)
27
+ }
28
+
29
+ @objc public func notification(_ options: NotificationOptions, completion: @escaping (Error?) -> Void) {
30
+ DispatchQueue.main.async {
31
+ let generator = UINotificationFeedbackGenerator()
32
+ generator.prepare()
33
+ generator.notificationOccurred(options.type)
34
+ completion(nil)
35
+ }
36
+ }
37
+
38
+ @objc public func playPattern(_ options: PlayPatternOptions, completion: @escaping (Error?) -> Void) {
39
+ do {
40
+ let pattern = try createPattern(from: options.events)
41
+ let engine = try getEngine()
42
+ let player = try engine.makePlayer(with: pattern)
43
+ try engine.start()
44
+ try player.start(atTime: CHHapticTimeImmediate)
45
+ completion(nil)
46
+ } catch {
47
+ completion(CustomError.patternPlaybackFailed)
48
+ }
49
+ }
50
+
51
+ @objc public func selectionChanged(completion: @escaping (Error?) -> Void) {
52
+ DispatchQueue.main.async {
53
+ let generator = self.selectionFeedbackGenerator ?? UISelectionFeedbackGenerator()
54
+ self.selectionFeedbackGenerator = generator
55
+ generator.selectionChanged()
56
+ completion(nil)
57
+ }
58
+ }
59
+
60
+ @objc public func selectionEnd(completion: @escaping (Error?) -> Void) {
61
+ DispatchQueue.main.async {
62
+ self.selectionFeedbackGenerator = nil
63
+ completion(nil)
64
+ }
65
+ }
66
+
67
+ @objc public func selectionStart(completion: @escaping (Error?) -> Void) {
68
+ DispatchQueue.main.async {
69
+ let generator = UISelectionFeedbackGenerator()
70
+ generator.prepare()
71
+ self.selectionFeedbackGenerator = generator
72
+ completion(nil)
73
+ }
74
+ }
75
+
76
+ @objc public func supportsHaptics() -> Bool {
77
+ return CHHapticEngine.capabilitiesForHardware().supportsHaptics
78
+ }
79
+
80
+ @objc public func vibrate(completion: @escaping (Error?) -> Void) {
81
+ AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
82
+ completion(nil)
83
+ }
84
+
85
+ private func createPattern(from events: [PlayPatternOptions.Event]) throws -> CHHapticPattern {
86
+ var hapticEvents: [CHHapticEvent] = []
87
+ for event in events {
88
+ let parameters = [
89
+ CHHapticEventParameter(parameterID: .hapticIntensity, value: event.intensity),
90
+ CHHapticEventParameter(parameterID: .hapticSharpness, value: event.sharpness)
91
+ ]
92
+ if let duration = event.duration {
93
+ hapticEvents.append(
94
+ CHHapticEvent(eventType: .hapticContinuous, parameters: parameters, relativeTime: event.time, duration: duration)
95
+ )
96
+ } else {
97
+ hapticEvents.append(CHHapticEvent(eventType: .hapticTransient, parameters: parameters, relativeTime: event.time))
98
+ }
99
+ }
100
+ return try CHHapticPattern(events: hapticEvents, parameters: [])
101
+ }
102
+
103
+ private func getEngine() throws -> CHHapticEngine {
104
+ if let engine = engine {
105
+ return engine
106
+ }
107
+ let engine = try CHHapticEngine()
108
+ engine.isAutoShutdownEnabled = true
109
+ engine.resetHandler = { [weak self] in
110
+ self?.engine = nil
111
+ }
112
+ self.engine = engine
113
+ return engine
114
+ }
115
+ }
@@ -0,0 +1,155 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc(HapticsPlugin)
5
+ public class HapticsPlugin: CAPPlugin, CAPBridgedPlugin {
6
+ public let identifier = "HapticsPlugin"
7
+ public let jsName = "Haptics"
8
+ public let pluginMethods: [CAPPluginMethod] = [
9
+ CAPPluginMethod(name: "impact", returnType: CAPPluginReturnPromise),
10
+ CAPPluginMethod(name: "isAvailable", returnType: CAPPluginReturnPromise),
11
+ CAPPluginMethod(name: "notification", returnType: CAPPluginReturnPromise),
12
+ CAPPluginMethod(name: "performAndroidHaptic", returnType: CAPPluginReturnPromise),
13
+ CAPPluginMethod(name: "playPattern", returnType: CAPPluginReturnPromise),
14
+ CAPPluginMethod(name: "selectionChanged", returnType: CAPPluginReturnPromise),
15
+ CAPPluginMethod(name: "selectionEnd", returnType: CAPPluginReturnPromise),
16
+ CAPPluginMethod(name: "selectionStart", returnType: CAPPluginReturnPromise),
17
+ CAPPluginMethod(name: "vibrate", returnType: CAPPluginReturnPromise)
18
+ ]
19
+ public static let tag = "HapticsPlugin"
20
+
21
+ private var implementation: Haptics?
22
+
23
+ override public func load() {
24
+ self.implementation = Haptics(plugin: self)
25
+ }
26
+
27
+ @objc func impact(_ call: CAPPluginCall) {
28
+ do {
29
+ let options = try ImpactOptions(call)
30
+ implementation?.impact(options, completion: { error in
31
+ if let error = error {
32
+ self.rejectCall(call, error)
33
+ return
34
+ }
35
+ self.resolveCall(call)
36
+ })
37
+ } catch {
38
+ rejectCall(call, error)
39
+ }
40
+ }
41
+
42
+ @objc func isAvailable(_ call: CAPPluginCall) {
43
+ implementation?.isAvailable(completion: { result, error in
44
+ if let error = error {
45
+ self.rejectCall(call, error)
46
+ return
47
+ }
48
+ self.resolveCall(call, result)
49
+ })
50
+ }
51
+
52
+ @objc func notification(_ call: CAPPluginCall) {
53
+ do {
54
+ let options = try NotificationOptions(call)
55
+ implementation?.notification(options, completion: { error in
56
+ if let error = error {
57
+ self.rejectCall(call, error)
58
+ return
59
+ }
60
+ self.resolveCall(call)
61
+ })
62
+ } catch {
63
+ rejectCall(call, error)
64
+ }
65
+ }
66
+
67
+ @objc func performAndroidHaptic(_ call: CAPPluginCall) {
68
+ rejectCallAsUnimplemented(call)
69
+ }
70
+
71
+ @objc func playPattern(_ call: CAPPluginCall) {
72
+ guard implementation?.supportsHaptics() == true else {
73
+ rejectCallAsUnavailable(call)
74
+ return
75
+ }
76
+ do {
77
+ let options = try PlayPatternOptions(call)
78
+ implementation?.playPattern(options, completion: { error in
79
+ if let error = error {
80
+ self.rejectCall(call, error)
81
+ return
82
+ }
83
+ self.resolveCall(call)
84
+ })
85
+ } catch {
86
+ rejectCall(call, error)
87
+ }
88
+ }
89
+
90
+ @objc func selectionChanged(_ call: CAPPluginCall) {
91
+ implementation?.selectionChanged(completion: { error in
92
+ if let error = error {
93
+ self.rejectCall(call, error)
94
+ return
95
+ }
96
+ self.resolveCall(call)
97
+ })
98
+ }
99
+
100
+ @objc func selectionEnd(_ call: CAPPluginCall) {
101
+ implementation?.selectionEnd(completion: { error in
102
+ if let error = error {
103
+ self.rejectCall(call, error)
104
+ return
105
+ }
106
+ self.resolveCall(call)
107
+ })
108
+ }
109
+
110
+ @objc func selectionStart(_ call: CAPPluginCall) {
111
+ implementation?.selectionStart(completion: { error in
112
+ if let error = error {
113
+ self.rejectCall(call, error)
114
+ return
115
+ }
116
+ self.resolveCall(call)
117
+ })
118
+ }
119
+
120
+ @objc func vibrate(_ call: CAPPluginCall) {
121
+ implementation?.vibrate(completion: { error in
122
+ if let error = error {
123
+ self.rejectCall(call, error)
124
+ return
125
+ }
126
+ self.resolveCall(call)
127
+ })
128
+ }
129
+
130
+ private func rejectCall(_ call: CAPPluginCall, _ error: Error) {
131
+ CAPLog.print("[", HapticsPlugin.tag, "] ", error)
132
+ let code = (error as? CustomError)?.code
133
+ call.reject(error.localizedDescription, code)
134
+ }
135
+
136
+ private func rejectCallAsUnavailable(_ call: CAPPluginCall) {
137
+ call.unavailable("This method is not available on this platform.")
138
+ }
139
+
140
+ private func rejectCallAsUnimplemented(_ call: CAPPluginCall) {
141
+ call.unimplemented("This method is not available on this platform.")
142
+ }
143
+
144
+ private func resolveCall(_ call: CAPPluginCall) {
145
+ call.resolve()
146
+ }
147
+
148
+ private func resolveCall(_ call: CAPPluginCall, _ result: Result?) {
149
+ if let result = result?.toJSObject() as? JSObject {
150
+ call.resolve(result)
151
+ } else {
152
+ call.resolve()
153
+ }
154
+ }
155
+ }
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
7
+ <key>CFBundleExecutable</key>
8
+ <string>$(EXECUTABLE_NAME)</string>
9
+ <key>CFBundleIdentifier</key>
10
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11
+ <key>CFBundleInfoDictionaryVersion</key>
12
+ <string>6.0</string>
13
+ <key>CFBundleName</key>
14
+ <string>$(PRODUCT_NAME)</string>
15
+ <key>CFBundlePackageType</key>
16
+ <string>FMWK</string>
17
+ <key>CFBundleShortVersionString</key>
18
+ <string>1.0</string>
19
+ <key>CFBundleVersion</key>
20
+ <string>$(CURRENT_PROJECT_VERSION)</string>
21
+ <key>NSPrincipalClass</key>
22
+ <string></string>
23
+ </dict>
24
+ </plist>
@@ -0,0 +1,5 @@
1
+ import Capacitor
2
+
3
+ @objc public protocol Result {
4
+ @objc func toJSObject() -> AnyObject
5
+ }
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "@capawesome/capacitor-haptics",
3
+ "version": "0.0.1",
4
+ "description": "Capacitor plugin to provide haptic feedback such as impacts, notifications, vibrations and custom haptic patterns.",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "android/src/main/",
11
+ "android/build.gradle",
12
+ "dist/",
13
+ "ios/Plugin/",
14
+ "CapawesomeCapacitorHaptics.podspec",
15
+ "Package.swift"
16
+ ],
17
+ "author": "Robin Genz <mail@robingenz.dev>",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/capawesome-team/capacitor-plugins.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/capawesome-team/capacitor-plugins/issues"
25
+ },
26
+ "funding": [
27
+ {
28
+ "type": "github",
29
+ "url": "https://github.com/sponsors/capawesome-team/"
30
+ },
31
+ {
32
+ "type": "opencollective",
33
+ "url": "https://opencollective.com/capawesome"
34
+ }
35
+ ],
36
+ "homepage": "https://capawesome.io/docs/sdks/capacitor/haptics/",
37
+ "keywords": [
38
+ "capacitor",
39
+ "plugin",
40
+ "native",
41
+ "haptics",
42
+ "haptic feedback",
43
+ "vibration",
44
+ "taptic engine",
45
+ "vibrate"
46
+ ],
47
+ "scripts": {
48
+ "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
49
+ "verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin -destination generic/platform=iOS && cd ..",
50
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
51
+ "verify:web": "npm run build",
52
+ "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
53
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
54
+ "eslint": "eslint . --ext ts",
55
+ "prettier": "prettier \"**/*.{css,html,ts,js,java}\"",
56
+ "swiftlint": "node-swiftlint",
57
+ "docgen": "docgen --api HapticsPlugin --output-readme README.md --output-json dist/docs.json",
58
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
59
+ "clean": "rimraf ./dist",
60
+ "watch": "tsc --watch",
61
+ "ios:pod:install": "cd ios && pod install --repo-update && cd ..",
62
+ "ios:spm:install": "cd ios && swift package resolve && cd ..",
63
+ "prepublishOnly": "npm run build"
64
+ },
65
+ "devDependencies": {
66
+ "@capacitor/android": "8.0.0",
67
+ "@capacitor/cli": "8.0.0",
68
+ "@capacitor/core": "8.0.0",
69
+ "@capacitor/docgen": "0.3.1",
70
+ "@capacitor/ios": "8.0.0",
71
+ "@ionic/eslint-config": "0.4.0",
72
+ "eslint": "8.57.0",
73
+ "prettier-plugin-java": "2.6.7",
74
+ "rimraf": "6.1.2",
75
+ "rollup": "4.53.3",
76
+ "swiftlint": "2.0.0",
77
+ "typescript": "5.9.3"
78
+ },
79
+ "peerDependencies": {
80
+ "@capacitor/core": ">=8.0.0"
81
+ },
82
+ "eslintConfig": {
83
+ "extends": "@ionic/eslint-config/recommended"
84
+ },
85
+ "capacitor": {
86
+ "ios": {
87
+ "src": "ios"
88
+ },
89
+ "android": {
90
+ "src": "android"
91
+ }
92
+ },
93
+ "publishConfig": {
94
+ "access": "public"
95
+ }
96
+ }