@jwplayer/jwplayer-react-native 1.0.0

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 (61) hide show
  1. package/.github/CODEOWNERS +2 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. package/.github/ISSUE_TEMPLATE/question.md +11 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +15 -0
  6. package/CODE_OF_CONDUCT.md +128 -0
  7. package/LICENSE +21 -0
  8. package/README.md +425 -0
  9. package/RNJWPlayer.podspec +44 -0
  10. package/android/.gradle/8.1.1/checksums/checksums.lock +0 -0
  11. package/android/.gradle/8.1.1/dependencies-accessors/dependencies-accessors.lock +0 -0
  12. package/android/.gradle/8.1.1/dependencies-accessors/gc.properties +0 -0
  13. package/android/.gradle/8.1.1/fileChanges/last-build.bin +0 -0
  14. package/android/.gradle/8.1.1/fileHashes/fileHashes.lock +0 -0
  15. package/android/.gradle/8.1.1/gc.properties +0 -0
  16. package/android/.gradle/8.2/checksums/checksums.lock +0 -0
  17. package/android/.gradle/8.2/dependencies-accessors/dependencies-accessors.lock +0 -0
  18. package/android/.gradle/8.2/dependencies-accessors/gc.properties +0 -0
  19. package/android/.gradle/8.2/fileChanges/last-build.bin +0 -0
  20. package/android/.gradle/8.2/fileHashes/fileHashes.lock +0 -0
  21. package/android/.gradle/8.2/gc.properties +0 -0
  22. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  23. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  24. package/android/.gradle/config.properties +2 -0
  25. package/android/.gradle/vcs-1/gc.properties +0 -0
  26. package/android/.idea/gradle.xml +12 -0
  27. package/android/.idea/migrations.xml +10 -0
  28. package/android/.idea/misc.xml +10 -0
  29. package/android/.idea/vcs.xml +6 -0
  30. package/android/.idea/workspace.xml +54 -0
  31. package/android/build.gradle +110 -0
  32. package/android/local.properties +8 -0
  33. package/android/src/main/AndroidManifest.xml +25 -0
  34. package/android/src/main/java/com/jwplayer/rnjwplayer/ArrayUtil.java +129 -0
  35. package/android/src/main/java/com/jwplayer/rnjwplayer/CastOptionsProvider.java +55 -0
  36. package/android/src/main/java/com/jwplayer/rnjwplayer/MapUtil.java +136 -0
  37. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayer.java +76 -0
  38. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerAds.java +239 -0
  39. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerModule.java +526 -0
  40. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerPackage.java +30 -0
  41. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java +1499 -0
  42. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerViewManager.java +171 -0
  43. package/android/src/main/java/com/jwplayer/rnjwplayer/Util.java +219 -0
  44. package/android/src/main/java/com/jwplayer/rnjwplayer/WidevineCallback.java +62 -0
  45. package/badges/license.svg +1 -0
  46. package/badges/version.svg +1 -0
  47. package/docs/legacy_readme.md +634 -0
  48. package/docs/props.md +43 -0
  49. package/docs/types.md +254 -0
  50. package/index.d.ts +564 -0
  51. package/index.js +699 -0
  52. package/ios/RNJWPlayer/RCTConvert+RNJWPlayer.swift +119 -0
  53. package/ios/RNJWPlayer/RNJWPlayer-Bridging-Header.h +5 -0
  54. package/ios/RNJWPlayer/RNJWPlayerAds.swift +260 -0
  55. package/ios/RNJWPlayer/RNJWPlayerModels.swift +149 -0
  56. package/ios/RNJWPlayer/RNJWPlayerView.swift +1837 -0
  57. package/ios/RNJWPlayer/RNJWPlayerViewController.swift +616 -0
  58. package/ios/RNJWPlayer/RNJWPlayerViewManager.m +132 -0
  59. package/ios/RNJWPlayer/RNJWPlayerViewManager.swift +500 -0
  60. package/ios/RNJWPlayer.xcodeproj/project.pbxproj +323 -0
  61. package/package.json +45 -0
@@ -0,0 +1,119 @@
1
+ //
2
+ // RNJWPlayerViewController.m
3
+ // RNJWPlayer
4
+ //
5
+ // Created by Chaim Paneth on 3/30/22.
6
+ //
7
+
8
+ import Foundation
9
+ import React
10
+ import JWPlayerKit
11
+
12
+ extension RCTConvert {
13
+
14
+ static func JWAdClient(_ value: String) -> JWAdClient {
15
+ switch value {
16
+ case "vast":
17
+ return .JWPlayer
18
+ case "ima":
19
+ return .GoogleIMA
20
+ case "ima_dai":
21
+ return .GoogleIMADAI
22
+ default:
23
+ return .unknown
24
+ }
25
+ }
26
+
27
+ static func JWInterfaceBehavior(_ value: String) -> JWInterfaceBehavior {
28
+ switch value {
29
+ case "normal":
30
+ return .normal
31
+ case "hidden":
32
+ return .hidden
33
+ case "onscreen":
34
+ return .alwaysOnScreen
35
+ default:
36
+ return .normal
37
+ }
38
+ }
39
+
40
+ static func JWCaptionEdgeStyle(_ value: String) -> JWCaptionEdgeStyle {
41
+ switch value {
42
+ case "none":
43
+ return .none
44
+ case "dropshadow":
45
+ return .dropshadow
46
+ case "raised":
47
+ return .raised
48
+ case "depressed":
49
+ return .depressed
50
+ case "uniform":
51
+ return .uniform
52
+ default:
53
+ return .undefined
54
+ }
55
+ }
56
+
57
+ static func JWPreload(_ value: String) -> JWPreload {
58
+ switch value {
59
+ case "auto":
60
+ return .auto
61
+ case "none":
62
+ return .none
63
+ default:
64
+ return .none
65
+ }
66
+ }
67
+
68
+ static func JWRelatedOnClick(_ value: String) -> JWRelatedOnClick {
69
+ switch value {
70
+ case "play":
71
+ return .play
72
+ case "link":
73
+ return .link
74
+ default:
75
+ return .play
76
+ }
77
+ }
78
+
79
+ static func JWRelatedOnComplete(_ value: String) -> JWRelatedOnComplete {
80
+ switch value {
81
+ case "show":
82
+ return .show
83
+ case "hide":
84
+ return .hide
85
+ case "autoplay":
86
+ return .autoplay
87
+ default:
88
+ return .show
89
+ }
90
+ }
91
+
92
+ static func JWControlType(_ value: String) -> JWControlType {
93
+ switch value {
94
+ case "forward":
95
+ return .fastForwardButton
96
+ case "rewind":
97
+ return .rewindButton
98
+ case "pip":
99
+ return .pictureInPictureButton
100
+ case "airplay":
101
+ return .airplayButton
102
+ case "chromecast":
103
+ return .chromecastButton
104
+ case "next":
105
+ return .nextButton
106
+ case "previous":
107
+ return .previousButton
108
+ case "settings":
109
+ return .settingsButton
110
+ case "languages":
111
+ return .languagesButton
112
+ case "fullscreen":
113
+ return .fullscreenButton
114
+ default:
115
+ return .fullscreenButton
116
+ }
117
+ }
118
+
119
+ }
@@ -0,0 +1,5 @@
1
+ #if __has_include("React/RCTViewManager.h")
2
+ #import "React/RCTViewManager.h"
3
+ #else
4
+ #import "RCTViewManager.h"
5
+ #endif
@@ -0,0 +1,260 @@
1
+ //
2
+ // RNJWPlayerAds.swift
3
+ // RNJWPlayer
4
+ //
5
+ // Created by Chaim Paneth on 24/12/2023.
6
+ //
7
+
8
+ import Foundation
9
+ import JWPlayerKit
10
+
11
+ class RNJWPlayerAds {
12
+
13
+ // Convert configureVASTWithAds function
14
+ static func configureVAST(with ads: [String: Any]) -> JWAdvertisingConfig? {
15
+ let adConfigBuilder = JWAdsAdvertisingConfigBuilder()
16
+
17
+ // Configure VAST specific settings here
18
+ // Example: setting ad schedule, tag, etc.
19
+
20
+ if let scheduleArray = getAdSchedule(from: ads), !scheduleArray.isEmpty {
21
+ adConfigBuilder.schedule(scheduleArray)
22
+ }
23
+
24
+ if let tag = ads["tag"] as? String, let encodedString = tag.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed), let tagUrl = URL(string: encodedString) {
25
+ adConfigBuilder.tag(tagUrl)
26
+ }
27
+
28
+ if let adVmap = ads["adVmap"] as? String, let encodedString = adVmap.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed), let adVmapUrl = URL(string: encodedString) {
29
+ adConfigBuilder.vmapURL(adVmapUrl)
30
+ }
31
+
32
+ if let openBrowserOnAdClick = ads["openBrowserOnAdClick"] as? Bool {
33
+ adConfigBuilder.openBrowserOnAdClick(openBrowserOnAdClick)
34
+ }
35
+
36
+ if let adRulesDict = ads["adRules"] as? [String: Any], let adRules = getAdRules(from: adRulesDict) {
37
+ adConfigBuilder.adRules(adRules)
38
+ }
39
+
40
+ if let adSettingsDict = ads["adSettings"] as? [String: Any] {
41
+ if let adSettings = getAdSettings(from: adSettingsDict) {
42
+ adConfigBuilder.adSettings(adSettings)
43
+ }
44
+ }
45
+
46
+ return try? adConfigBuilder.build()
47
+ }
48
+
49
+ // Convert configureIMAWithAds function
50
+ static func configureIMA(with ads: [String: Any]) -> JWAdvertisingConfig? {
51
+ // Ensure Google IMA SDK is available
52
+ #if !USE_GOOGLE_IMA
53
+ assertionFailure("Error: GoogleAds-IMA-iOS-SDK is not installed. Add $RNJWPlayerUseGoogleIMA = true; to your podfile")
54
+ #endif
55
+
56
+ let adConfigBuilder = JWImaAdvertisingConfigBuilder()
57
+
58
+ // Configure Google IMA specific settings here
59
+ // Example: setting ad schedule, tag, IMA settings, etc.
60
+
61
+ if let scheduleArray = getAdSchedule(from: ads), !scheduleArray.isEmpty {
62
+ adConfigBuilder.schedule(scheduleArray)
63
+ }
64
+
65
+ if let tag = ads["tag"] as? String, let encodedString = tag.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed), let tagUrl = URL(string: encodedString) {
66
+ adConfigBuilder.tag(tagUrl)
67
+ }
68
+
69
+ if let adVmap = ads["adVmap"] as? String, let encodedString = adVmap.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed), let adVmapUrl = URL(string: encodedString) {
70
+ adConfigBuilder.vmapURL(adVmapUrl)
71
+ }
72
+
73
+ if let adRulesDict = ads["adRules"] as? [String: Any], let adRules = getAdRules(from: adRulesDict) {
74
+ adConfigBuilder.adRules(adRules)
75
+ }
76
+
77
+ if let imaSettingsDict = ads["imaSettings"] as? [String: Any] {
78
+ if let imaSettings = getIMASettings(from: imaSettingsDict) {
79
+ adConfigBuilder.imaSettings(imaSettings)
80
+ }
81
+ }
82
+
83
+ return try? adConfigBuilder.build()
84
+ }
85
+
86
+ // Convert configureIMADAIWithAds function
87
+ static func configureIMADAI(with ads: [String: Any]) -> JWAdvertisingConfig? {
88
+ // Ensure Google IMA SDK is available
89
+ #if !USE_GOOGLE_IMA
90
+ assertionFailure("Error: GoogleAds-IMA-iOS-SDK is not installed. Add $RNJWPlayerUseGoogleIMA = true; to your podfile")
91
+ #endif
92
+
93
+ let adConfigBuilder = JWImaDaiAdvertisingConfigBuilder()
94
+
95
+ // Configure Google IMA DAI specific settings here
96
+ // Example: setting stream configuration, friendly obstructions, etc.
97
+
98
+ if let imaSettingsDict = ads["imaSettings"] as? [String: Any] {
99
+ if let imaSettings = getIMASettings(from: imaSettingsDict) {
100
+ adConfigBuilder.imaSettings(imaSettings)
101
+ }
102
+ }
103
+
104
+ if let googleDAIStreamDict = ads["googleDAIStream"] as? [String: Any], let googleDAIStream = getGoogleDAIStream(from: googleDAIStreamDict) {
105
+ adConfigBuilder.googleDAIStream(googleDAIStream)
106
+ }
107
+
108
+ return try? adConfigBuilder.build()
109
+ }
110
+
111
+ // Convert getAdSchedule function
112
+ static func getAdSchedule(from ads: [String: Any]) -> [JWAdBreak]? {
113
+ guard let schedule = ads["adSchedule"] as? [[String: Any]], !schedule.isEmpty else { return nil }
114
+
115
+ var scheduleArray: [JWAdBreak] = []
116
+
117
+ for item in schedule {
118
+ if let offsetString = item["offset"] as? String, let tag = item["tag"] as? String, let encodedString = tag.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed), let tagUrl = URL(string: encodedString) {
119
+
120
+ let adBreakBuilder = JWAdBreakBuilder()
121
+ if let offset = JWAdOffset.from(string: offsetString) {
122
+ adBreakBuilder.offset(offset)
123
+ adBreakBuilder.tags([tagUrl])
124
+
125
+ if let adBreak = try? adBreakBuilder.build() {
126
+ scheduleArray.append(adBreak)
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ return scheduleArray.isEmpty ? nil : scheduleArray
133
+ }
134
+
135
+ // Convert getAdRules function
136
+ static func getAdRules(from adRulesDict: [String: Any]) -> JWAdRules? {
137
+ let builder = JWAdRulesBuilder()
138
+
139
+ if let startOn = adRulesDict["startOn"] as? UInt, let frequency = adRulesDict["frequency"] as? UInt, let timeBetweenAds = adRulesDict["timeBetweenAds"] as? UInt {
140
+ let startOnSeek = mapStringToJWAdShownOnSeek(adRulesDict["startOnSeek"] as? String)
141
+ builder.jwRules(startOn: startOn, frequency: frequency, timeBetweenAds: timeBetweenAds, startOnSeek: startOnSeek)
142
+ }
143
+
144
+ return try? builder.build()
145
+ }
146
+
147
+ // Convert mapStringToJWAdShownOnSeek function
148
+ static func mapStringToJWAdShownOnSeek(_ seekString: String?) -> JWAdShownOnSeek {
149
+ guard let seekString = seekString else { return .none }
150
+ switch seekString {
151
+ case "pre":
152
+ return .pre
153
+ default:
154
+ return .none
155
+ }
156
+ }
157
+
158
+ // Convert getAdSettings function
159
+ static func getAdSettings(from settingsDict: [String: Any]) -> JWAdSettings? {
160
+ let builder = JWAdSettingsBuilder()
161
+
162
+ if let allowsBackgroundPlayback = settingsDict["allowsBackgroundPlayback"] as? Bool {
163
+ builder.allowsBackgroundPlayback(allowsBackgroundPlayback)
164
+ }
165
+
166
+ // Add other settings as needed
167
+
168
+ return builder.build()
169
+ }
170
+
171
+ // Convert getIMASettings function
172
+ static func getIMASettings(from imaSettingsDict: [String: Any]) -> JWImaSettings? {
173
+ let builder = JWImaSettingsBuilder()
174
+ if let locale = imaSettingsDict["locale"] as? String {
175
+ builder.locale(locale)
176
+ }
177
+ if let ppid = imaSettingsDict["ppid"] as? String {
178
+ builder.ppid(ppid)
179
+ }
180
+ if let maxRedirects = imaSettingsDict["maxRedirects"] as? UInt {
181
+ builder.maxRedirects(maxRedirects)
182
+ }
183
+ if let sessionID = imaSettingsDict["sessionID"] as? String {
184
+ builder.sessionID(sessionID)
185
+ }
186
+ if let debugMode = imaSettingsDict["debugMode"] as? Bool {
187
+ builder.debugMode(debugMode)
188
+ }
189
+ return builder.build()
190
+ }
191
+
192
+ // Convert getGoogleDAIStream function
193
+ static func getGoogleDAIStream(from googleDAIStreamDict: [String: Any]) -> JWGoogleDAIStream? {
194
+ let builder = JWGoogleDAIStreamBuilder()
195
+ if let videoID = googleDAIStreamDict["videoID"] as? String, let cmsID = googleDAIStreamDict["cmsID"] as? String {
196
+ builder.vodStreamInfo(videoID: videoID, cmsID: cmsID)
197
+ } else if let assetKey = googleDAIStreamDict["assetKey"] as? String {
198
+ builder.liveStreamInfo(assetKey: assetKey)
199
+ }
200
+ if let apiKey = googleDAIStreamDict["apiKey"] as? String {
201
+ builder.apiKey(apiKey)
202
+ }
203
+ if let adTagParameters = googleDAIStreamDict["adTagParameters"] as? [String: Any] {
204
+ let stringParams = adTagParameters.compactMapValues { $0 as? String }
205
+ builder.adTagParameters(stringParams)
206
+ }
207
+ return try? builder.build()
208
+ }
209
+
210
+ // Placeholder for findViewWithId function - Needs implementation
211
+ // static func findView(withId viewId: String) -> UIView? {
212
+ // // Implementation needed to find and return the view with the given id
213
+ // return nil
214
+ // }
215
+ //
216
+ // static func createFriendlyObstructions(fromArray obstructionsArray: [[String: Any]]) -> [JWFriendlyObstruction] {
217
+ // var obstructions: [JWFriendlyObstruction] = []
218
+ //
219
+ // for obstructionDict in obstructionsArray {
220
+ // if let viewId = obstructionDict["viewId"] as? String,
221
+ // let view = findView(withId: viewId),
222
+ // let purposeString = obstructionDict["purpose"] as? String,
223
+ // let reason = obstructionDict["reason"] as? String {
224
+ // let purpose = mapStringToJWFriendlyObstructionPurpose(purposeString)
225
+ // let obstruction = JWFriendlyObstruction(view: view, purpose: purpose, reason: reason)
226
+ // obstructions.append(obstruction)
227
+ // }
228
+ // }
229
+ //
230
+ // return obstructions
231
+ // }
232
+ //
233
+ // static func mapStringToJWFriendlyObstructionPurpose(_ purposeString: String) -> JWFriendlyObstructionPurpose {
234
+ // switch purposeString {
235
+ // case "mediaControls":
236
+ // return .mediaControls
237
+ // case "closeAd":
238
+ // return .closeAd
239
+ // case "notVisible":
240
+ // return .notVisible
241
+ // default:
242
+ // return .other
243
+ // }
244
+ // }
245
+ //
246
+ // static func createCompanionAdSlot(fromDictionary companionAdSlotDict: [String: Any]) -> JWCompanionAdSlot? {
247
+ // guard let viewId = companionAdSlotDict["viewId"] as? String,
248
+ // let view = findView(withId: viewId),
249
+ // let sizeDict = companionAdSlotDict["size"] as? [String: Float],
250
+ // let width = sizeDict["width"],
251
+ // let height = sizeDict["height"] else {
252
+ // return nil
253
+ // }
254
+ //
255
+ // let size = CGSize(width: CGFloat(width), height: CGFloat(height))
256
+ // let slot = JWCompanionAdSlot(view: view, size: size)
257
+ // return slot
258
+ // }
259
+ }
260
+
@@ -0,0 +1,149 @@
1
+ // This file was generated from JSON Schema using quicktype, do not modify it directly.
2
+ // To parse the JSON, add this file to your project and do:
3
+ //
4
+ // let jWConfig = try? JSONDecoder().decode(JWConfig.self, from: jsonData)
5
+
6
+ import Foundation
7
+
8
+ // MARK: - JWConfig
9
+ struct JWConfig: Codable {
10
+ let config: Config
11
+ }
12
+
13
+ // MARK: - Config
14
+ struct Config: Codable {
15
+ let license: String
16
+ let advertising: Advertising
17
+ let autostart, controls, configRepeat: Bool
18
+ let nextUpStyle: NextUpStyle
19
+ let styling: Styling
20
+ let backgroundAudioEnabled: Bool
21
+ let category: String
22
+ let categoryOptions: [String]
23
+ let mode: String
24
+ let fullScreenOnLandscape, landscapeOnFullScreen, portraitOnExitFullScreen, exitFullScreenOnPortrait: Bool
25
+ let playlist: [Playlist]
26
+ let stretching: String
27
+ let related: Related
28
+ let preload, interfaceBehavior: String
29
+ let interfaceFadeDelay: Int
30
+ let hideUIGroups: [String]
31
+ let processSpcURL, fairplayCERTURL, contentUUID: String
32
+ let viewOnly, enableLockScreenControls, pipEnabled: Bool
33
+
34
+ enum CodingKeys: String, CodingKey {
35
+ case license, advertising, autostart, controls
36
+ case configRepeat = "repeat"
37
+ case nextUpStyle, styling, backgroundAudioEnabled, category, categoryOptions, mode, fullScreenOnLandscape, landscapeOnFullScreen, portraitOnExitFullScreen, exitFullScreenOnPortrait, playlist, stretching, related, preload, interfaceBehavior, interfaceFadeDelay, hideUIGroups
38
+ case processSpcURL = "processSpcUrl"
39
+ case fairplayCERTURL = "fairplayCertUrl"
40
+ case contentUUID, viewOnly, enableLockScreenControls, pipEnabled
41
+ }
42
+ }
43
+
44
+ // MARK: - Advertising
45
+ struct Advertising: Codable {
46
+ let adSchedule: [AdSchedule]
47
+ let adVmap, tag: String
48
+ let openBrowserOnAdClick: Bool
49
+ let adClient: String
50
+ }
51
+
52
+ // MARK: - AdSchedule
53
+ struct AdSchedule: Codable {
54
+ let tag, offset: String
55
+ }
56
+
57
+ // MARK: - NextUpStyle
58
+ struct NextUpStyle: Codable {
59
+ let offsetSeconds, offsetPercentage: Int
60
+ }
61
+
62
+ // MARK: - Playlist
63
+ struct Playlist: Codable {
64
+ let file: String
65
+ let sources: [Source]
66
+ let image, title, description, mediaID: String
67
+ let adSchedule: [AdSchedule]
68
+ let adVmap: String
69
+ let tracks: [Source]
70
+ let recommendations, startTime: String
71
+ let autostart: Bool
72
+
73
+ enum CodingKeys: String, CodingKey {
74
+ case file, sources, image, title, description
75
+ case mediaID = "mediaId"
76
+ case adSchedule, adVmap, tracks, recommendations, startTime, autostart
77
+ }
78
+ }
79
+
80
+ // MARK: - Source
81
+ struct Source: Codable {
82
+ let file, label: String
83
+ let sourceDefault: Bool
84
+
85
+ enum CodingKeys: String, CodingKey {
86
+ case file, label
87
+ case sourceDefault = "default"
88
+ }
89
+ }
90
+
91
+ // MARK: - Related
92
+ struct Related: Codable {
93
+ let onClick, onComplete, heading, url: String
94
+ let autoplayMessage: String
95
+ let autoplayTimer: Int
96
+ }
97
+
98
+ // MARK: - Styling
99
+ struct Styling: Codable {
100
+ let colors: Colors
101
+ let font: Font
102
+ let displayTitle, displayDescription: Bool
103
+ let captionsStyle: CaptionsStyle
104
+ let menuStyle: MenuStyle
105
+ }
106
+
107
+ // MARK: - CaptionsStyle
108
+ struct CaptionsStyle: Codable {
109
+ let font: Font
110
+ let fontColor, backgroundColor, highlightColor, edgeStyle: String
111
+ }
112
+
113
+ // MARK: - Font
114
+ struct Font: Codable {
115
+ let name: String
116
+ let size: Int
117
+ }
118
+
119
+ // MARK: - Colors
120
+ struct Colors: Codable {
121
+ let buttons, backgroundColor, fontColor: String
122
+ let timeslider: Timeslider
123
+ }
124
+
125
+ // MARK: - Timeslider
126
+ struct Timeslider: Codable {
127
+ let progress, rail, thumb: String
128
+ }
129
+
130
+ // MARK: - MenuStyle
131
+ struct MenuStyle: Codable {
132
+ let font: Font
133
+ let fontColor, backgroundColor: String
134
+ }
135
+
136
+ // MARK: - Extensions
137
+ extension Decodable {
138
+ init<Key: Hashable, Value>(_ dict: [Key: Value]) throws where Key: Codable, Value: Codable {
139
+ let data = try JSONEncoder().encode(dict)
140
+ self = try JSONDecoder().decode(Self.self, from: data)
141
+ }
142
+ }
143
+
144
+ extension Decodable {
145
+ init<Key: Hashable>(_ dict: [Key: Any]) throws {
146
+ let data = try JSONSerialization.data(withJSONObject: dict, options: [])
147
+ self = try JSONDecoder().decode(Self.self, from: data)
148
+ }
149
+ }