@capawesome/capacitor-volume 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 (49) hide show
  1. package/CapawesomeCapacitorVolume.podspec +17 -0
  2. package/LICENSE +21 -0
  3. package/Package.swift +28 -0
  4. package/README.md +405 -0
  5. package/android/build.gradle +58 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/Volume.java +158 -0
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/VolumeHelper.java +27 -0
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/VolumePlugin.java +178 -0
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/classes/CustomException.java +20 -0
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/classes/CustomExceptions.java +12 -0
  12. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/classes/events/VolumeButtonPressedEvent.java +23 -0
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/classes/events/VolumeChangeEvent.java +22 -0
  14. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/classes/options/GetVolumeOptions.java +23 -0
  15. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/classes/options/SetVolumeOptions.java +41 -0
  16. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/classes/options/StartWatchingOptions.java +17 -0
  17. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/classes/results/GetVolumeResult.java +22 -0
  18. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/classes/results/IsWatchingResult.java +22 -0
  19. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/interfaces/Callback.java +5 -0
  20. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/interfaces/EmptyCallback.java +5 -0
  21. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/interfaces/NonEmptyResultCallback.java +7 -0
  22. package/android/src/main/java/io/capawesome/capacitorjs/plugins/volume/interfaces/Result.java +7 -0
  23. package/android/src/main/res/.gitkeep +0 -0
  24. package/dist/docs.json +540 -0
  25. package/dist/esm/definitions.d.ts +255 -0
  26. package/dist/esm/definitions.js +60 -0
  27. package/dist/esm/definitions.js.map +1 -0
  28. package/dist/esm/index.d.ts +4 -0
  29. package/dist/esm/index.js +7 -0
  30. package/dist/esm/index.js.map +1 -0
  31. package/dist/esm/web.d.ts +9 -0
  32. package/dist/esm/web.js +19 -0
  33. package/dist/esm/web.js.map +1 -0
  34. package/dist/plugin.cjs.js +93 -0
  35. package/dist/plugin.cjs.js.map +1 -0
  36. package/dist/plugin.js +96 -0
  37. package/dist/plugin.js.map +1 -0
  38. package/ios/Plugin/Classes/Events/VolumeButtonPressedEvent.swift +16 -0
  39. package/ios/Plugin/Classes/Events/VolumeChangeEvent.swift +16 -0
  40. package/ios/Plugin/Classes/Options/SetVolumeOptions.swift +20 -0
  41. package/ios/Plugin/Classes/Options/StartWatchingOptions.swift +10 -0
  42. package/ios/Plugin/Classes/Results/GetVolumeResult.swift +16 -0
  43. package/ios/Plugin/Classes/Results/IsWatchingResult.swift +16 -0
  44. package/ios/Plugin/Enums/CustomError.swift +24 -0
  45. package/ios/Plugin/Info.plist +24 -0
  46. package/ios/Plugin/Protocols/Result.swift +5 -0
  47. package/ios/Plugin/Volume.swift +192 -0
  48. package/ios/Plugin/VolumePlugin.swift +109 -0
  49. package/package.json +91 -0
@@ -0,0 +1,16 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc public class IsWatchingResult: NSObject, Result {
5
+ let watching: Bool
6
+
7
+ init(watching: Bool) {
8
+ self.watching = watching
9
+ }
10
+
11
+ @objc public func toJSObject() -> AnyObject {
12
+ var result = JSObject()
13
+ result["watching"] = watching
14
+ return result as AnyObject
15
+ }
16
+ }
@@ -0,0 +1,24 @@
1
+ import Foundation
2
+
3
+ enum CustomError: Error {
4
+ case volumeInvalid
5
+ case volumeMissing
6
+
7
+ var code: String? {
8
+ switch self {
9
+ case .volumeInvalid, .volumeMissing:
10
+ return nil
11
+ }
12
+ }
13
+ }
14
+
15
+ extension CustomError: LocalizedError {
16
+ public var errorDescription: String? {
17
+ switch self {
18
+ case .volumeInvalid:
19
+ return NSLocalizedString("volume must be between 0 and 1.", comment: "volumeInvalid")
20
+ case .volumeMissing:
21
+ return NSLocalizedString("volume must be provided.", comment: "volumeMissing")
22
+ }
23
+ }
24
+ }
@@ -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
+ }
@@ -0,0 +1,192 @@
1
+ import AVFoundation
2
+ import Capacitor
3
+ import Foundation
4
+ import MediaPlayer
5
+ import UIKit
6
+
7
+ @objc public class Volume: NSObject {
8
+ private static let directionDown = "down"
9
+ private static let directionUp = "up"
10
+ private static let maximumSuppressedVolume: Float = 0.875
11
+ private static let minimumSuppressedVolume: Float = 0.125
12
+ private static let minimumVolumeDelta: Float = 0.001
13
+ private static let programmaticChangeTolerance: Float = 0.05
14
+ private static let sliderSetDelay: TimeInterval = 0.1
15
+ private static let volumeViewRemovalDelay: TimeInterval = 0.5
16
+
17
+ private let audioSession = AVAudioSession.sharedInstance()
18
+ private let plugin: VolumePlugin
19
+
20
+ private var initialVolume: Float = 0
21
+ private var lastVolume: Float = 0
22
+ private var pendingSetVolume: Float?
23
+ private var suppressVolumeChange = false
24
+ private var volumeObservation: NSKeyValueObservation?
25
+ private var volumeView: MPVolumeView?
26
+ private var watching = false
27
+
28
+ init(plugin: VolumePlugin) {
29
+ self.plugin = plugin
30
+ }
31
+
32
+ deinit {
33
+ volumeObservation?.invalidate()
34
+ NotificationCenter.default.removeObserver(self)
35
+ }
36
+
37
+ @objc public func getVolume(completion: @escaping (_ result: GetVolumeResult?, _ error: Error?) -> Void) {
38
+ try? activateAudioSession()
39
+ completion(GetVolumeResult(volume: audioSession.outputVolume), nil)
40
+ }
41
+
42
+ @objc public func isWatching(completion: @escaping (_ result: IsWatchingResult?, _ error: Error?) -> Void) {
43
+ DispatchQueue.main.async {
44
+ completion(IsWatchingResult(watching: self.watching), nil)
45
+ }
46
+ }
47
+
48
+ @objc public func setVolume(_ options: SetVolumeOptions, completion: @escaping (_ error: Error?) -> Void) {
49
+ DispatchQueue.main.async {
50
+ self.setSystemVolume(options.volume)
51
+ completion(nil)
52
+ }
53
+ }
54
+
55
+ @objc public func startWatching(_ options: StartWatchingOptions, completion: @escaping (_ error: Error?) -> Void) {
56
+ DispatchQueue.main.async {
57
+ if self.watching {
58
+ completion(nil)
59
+ return
60
+ }
61
+ do {
62
+ try self.activateAudioSession()
63
+ } catch {
64
+ completion(error)
65
+ return
66
+ }
67
+ self.suppressVolumeChange = options.suppressVolumeChange
68
+ self.initialVolume = self.audioSession.outputVolume
69
+ self.lastVolume = self.initialVolume
70
+ self.watching = true
71
+ if self.suppressVolumeChange {
72
+ self.addVolumeView()
73
+ self.applySuppressedVolume(self.initialVolume)
74
+ }
75
+ self.volumeObservation = self.audioSession.observe(\.outputVolume, options: [.new]) { [weak self] _, change in
76
+ guard let newVolume = change.newValue else {
77
+ return
78
+ }
79
+ DispatchQueue.main.async {
80
+ self?.handleVolumeChanged(newVolume)
81
+ }
82
+ }
83
+ NotificationCenter.default.addObserver(
84
+ self,
85
+ selector: #selector(self.handleDidBecomeActive),
86
+ name: UIApplication.didBecomeActiveNotification,
87
+ object: nil
88
+ )
89
+ completion(nil)
90
+ }
91
+ }
92
+
93
+ @objc public func stopWatching(completion: ((_ error: Error?) -> Void)? = nil) {
94
+ DispatchQueue.main.async {
95
+ if !self.watching {
96
+ completion?(nil)
97
+ return
98
+ }
99
+ self.volumeObservation?.invalidate()
100
+ self.volumeObservation = nil
101
+ NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
102
+ self.watching = false
103
+ if self.suppressVolumeChange {
104
+ self.suppressVolumeChange = false
105
+ self.setSystemVolume(self.initialVolume)
106
+ }
107
+ self.pendingSetVolume = nil
108
+ try? self.audioSession.setActive(false, options: .notifyOthersOnDeactivation)
109
+ completion?(nil)
110
+ }
111
+ }
112
+
113
+ private func activateAudioSession() throws {
114
+ try audioSession.setCategory(.ambient, options: .mixWithOthers)
115
+ try audioSession.setActive(true)
116
+ }
117
+
118
+ private func addVolumeView() {
119
+ guard volumeView == nil else {
120
+ return
121
+ }
122
+ let volumeView = MPVolumeView(frame: CGRect(x: -3000, y: -3000, width: 100, height: 100))
123
+ plugin.bridge?.viewController?.view.addSubview(volumeView)
124
+ self.volumeView = volumeView
125
+ }
126
+
127
+ private func applySuppressedVolume(_ volume: Float) {
128
+ let suppressedVolume = min(max(volume, Self.minimumSuppressedVolume), Self.maximumSuppressedVolume)
129
+ if suppressedVolume != volume {
130
+ setSystemVolume(suppressedVolume)
131
+ }
132
+ }
133
+
134
+ @objc private func handleDidBecomeActive() {
135
+ try? activateAudioSession()
136
+ lastVolume = audioSession.outputVolume
137
+ if suppressVolumeChange {
138
+ applySuppressedVolume(lastVolume)
139
+ }
140
+ }
141
+
142
+ private func handleVolumeChanged(_ newVolume: Float) {
143
+ if let pendingSetVolume = pendingSetVolume, abs(newVolume - pendingSetVolume) < Self.programmaticChangeTolerance {
144
+ self.pendingSetVolume = nil
145
+ let changed = abs(newVolume - lastVolume) >= Self.minimumVolumeDelta
146
+ lastVolume = newVolume
147
+ if changed && !suppressVolumeChange {
148
+ plugin.notifyVolumeChangeListeners(VolumeChangeEvent(volume: newVolume))
149
+ }
150
+ return
151
+ }
152
+ let delta = newVolume - lastVolume
153
+ if abs(delta) < Self.minimumVolumeDelta {
154
+ return
155
+ }
156
+ let direction = delta > 0 ? Self.directionUp : Self.directionDown
157
+ plugin.notifyVolumeButtonPressedListeners(VolumeButtonPressedEvent(direction: direction))
158
+ if suppressVolumeChange {
159
+ setSystemVolume(lastVolume)
160
+ } else {
161
+ lastVolume = newVolume
162
+ plugin.notifyVolumeChangeListeners(VolumeChangeEvent(volume: newVolume))
163
+ }
164
+ }
165
+
166
+ private func removeVolumeView() {
167
+ volumeView?.removeFromSuperview()
168
+ volumeView = nil
169
+ }
170
+
171
+ private func setSystemVolume(_ volume: Float) {
172
+ addVolumeView()
173
+ if watching {
174
+ pendingSetVolume = volume
175
+ }
176
+ DispatchQueue.main.asyncAfter(deadline: .now() + Self.sliderSetDelay) { [weak self] in
177
+ guard let self = self else {
178
+ return
179
+ }
180
+ let slider = self.volumeView?.subviews.compactMap { $0 as? UISlider }.first
181
+ slider?.value = volume
182
+ if !(self.watching && self.suppressVolumeChange) {
183
+ DispatchQueue.main.asyncAfter(deadline: .now() + Self.volumeViewRemovalDelay) { [weak self] in
184
+ guard let self = self, !(self.watching && self.suppressVolumeChange) else {
185
+ return
186
+ }
187
+ self.removeVolumeView()
188
+ }
189
+ }
190
+ }
191
+ }
192
+ }
@@ -0,0 +1,109 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc(VolumePlugin)
5
+ public class VolumePlugin: CAPPlugin, CAPBridgedPlugin {
6
+ public let identifier = "VolumePlugin"
7
+ public let jsName = "Volume"
8
+ public let pluginMethods: [CAPPluginMethod] = [
9
+ CAPPluginMethod(name: "getVolume", returnType: CAPPluginReturnPromise),
10
+ CAPPluginMethod(name: "isWatching", returnType: CAPPluginReturnPromise),
11
+ CAPPluginMethod(name: "setVolume", returnType: CAPPluginReturnPromise),
12
+ CAPPluginMethod(name: "startWatching", returnType: CAPPluginReturnPromise),
13
+ CAPPluginMethod(name: "stopWatching", returnType: CAPPluginReturnPromise)
14
+ ]
15
+
16
+ public static let eventVolumeButtonPressed = "volumeButtonPressed"
17
+ public static let eventVolumeChange = "volumeChange"
18
+ public static let tag = "VolumePlugin"
19
+
20
+ private var implementation: Volume?
21
+
22
+ @objc func getVolume(_ call: CAPPluginCall) {
23
+ implementation?.getVolume(completion: { result, error in
24
+ if let error = error {
25
+ self.rejectCall(call, error)
26
+ return
27
+ }
28
+ self.resolveCall(call, result)
29
+ })
30
+ }
31
+
32
+ @objc func isWatching(_ call: CAPPluginCall) {
33
+ implementation?.isWatching(completion: { result, error in
34
+ if let error = error {
35
+ self.rejectCall(call, error)
36
+ return
37
+ }
38
+ self.resolveCall(call, result)
39
+ })
40
+ }
41
+
42
+ override public func load() {
43
+ self.implementation = Volume(plugin: self)
44
+ }
45
+
46
+ @objc public func notifyVolumeButtonPressedListeners(_ event: VolumeButtonPressedEvent) {
47
+ self.notifyListeners(Self.eventVolumeButtonPressed, data: event.toJSObject() as? [String: Any])
48
+ }
49
+
50
+ @objc public func notifyVolumeChangeListeners(_ event: VolumeChangeEvent) {
51
+ self.notifyListeners(Self.eventVolumeChange, data: event.toJSObject() as? [String: Any])
52
+ }
53
+
54
+ @objc func setVolume(_ call: CAPPluginCall) {
55
+ do {
56
+ let options = try SetVolumeOptions(call)
57
+
58
+ implementation?.setVolume(options, completion: { error in
59
+ if let error = error {
60
+ self.rejectCall(call, error)
61
+ return
62
+ }
63
+ self.resolveCall(call)
64
+ })
65
+ } catch {
66
+ rejectCall(call, error)
67
+ }
68
+ }
69
+
70
+ @objc func startWatching(_ call: CAPPluginCall) {
71
+ let options = StartWatchingOptions(call)
72
+
73
+ implementation?.startWatching(options, completion: { error in
74
+ if let error = error {
75
+ self.rejectCall(call, error)
76
+ return
77
+ }
78
+ self.resolveCall(call)
79
+ })
80
+ }
81
+
82
+ @objc func stopWatching(_ call: CAPPluginCall) {
83
+ implementation?.stopWatching { error in
84
+ if let error = error {
85
+ self.rejectCall(call, error)
86
+ return
87
+ }
88
+ self.resolveCall(call)
89
+ }
90
+ }
91
+
92
+ private func rejectCall(_ call: CAPPluginCall, _ error: Error) {
93
+ CAPLog.print("[", VolumePlugin.tag, "] ", error)
94
+ let code = (error as? CustomError)?.code
95
+ call.reject(error.localizedDescription, code)
96
+ }
97
+
98
+ private func resolveCall(_ call: CAPPluginCall) {
99
+ call.resolve()
100
+ }
101
+
102
+ private func resolveCall(_ call: CAPPluginCall, _ result: Result?) {
103
+ if let result = result?.toJSObject() as? JSObject {
104
+ call.resolve(result)
105
+ } else {
106
+ call.resolve()
107
+ }
108
+ }
109
+ }
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@capawesome/capacitor-volume",
3
+ "version": "0.0.1",
4
+ "description": "Capacitor plugin to control the volume and observe hardware volume button presses.",
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
+ "CapawesomeCapacitorVolume.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/plugins/volume/",
37
+ "keywords": [
38
+ "capacitor",
39
+ "plugin",
40
+ "native"
41
+ ],
42
+ "scripts": {
43
+ "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
44
+ "verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin -destination generic/platform=iOS && cd ..",
45
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
46
+ "verify:web": "npm run build",
47
+ "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
48
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
49
+ "eslint": "eslint . --ext ts",
50
+ "prettier": "prettier \"**/*.{css,html,ts,js,java}\"",
51
+ "swiftlint": "node-swiftlint",
52
+ "docgen": "docgen --api VolumePlugin --output-readme README.md --output-json dist/docs.json",
53
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
54
+ "clean": "rimraf ./dist",
55
+ "watch": "tsc --watch",
56
+ "ios:pod:install": "cd ios && pod install --repo-update && cd ..",
57
+ "ios:spm:install": "cd ios && swift package resolve && cd ..",
58
+ "prepublishOnly": "npm run build"
59
+ },
60
+ "devDependencies": {
61
+ "@capacitor/android": "8.0.0",
62
+ "@capacitor/cli": "8.0.0",
63
+ "@capacitor/core": "8.0.0",
64
+ "@capacitor/docgen": "0.3.1",
65
+ "@capacitor/ios": "8.0.0",
66
+ "@ionic/eslint-config": "0.4.0",
67
+ "eslint": "8.57.0",
68
+ "prettier-plugin-java": "2.6.7",
69
+ "rimraf": "6.1.2",
70
+ "rollup": "4.53.3",
71
+ "swiftlint": "2.0.0",
72
+ "typescript": "5.9.3"
73
+ },
74
+ "peerDependencies": {
75
+ "@capacitor/core": ">=8.0.0"
76
+ },
77
+ "eslintConfig": {
78
+ "extends": "@ionic/eslint-config/recommended"
79
+ },
80
+ "capacitor": {
81
+ "ios": {
82
+ "src": "ios"
83
+ },
84
+ "android": {
85
+ "src": "android"
86
+ }
87
+ },
88
+ "publishConfig": {
89
+ "access": "public"
90
+ }
91
+ }