1 // Copyright 2015-present 650 Industries. All rights reserved.
2 
3 import EXDevMenuInterface
4 
5 @objc(DevMenuExtensions)
6 open class DevMenuExtensions: NSObject, DevMenuExtensionProtocol {
7 
8   @objc
9   open var bridge: RCTBridge?
10 
11   // MARK: DevMenuExtensionProtocol
12 
13   @objc
14   open func devMenuItems(_ settings: DevMenuExtensionSettingsProtocol) -> DevMenuItemsContainerProtocol? {
15     if (!settings.wasRunOnDevelopmentBridge()) {
16       return nil
17     }
18 
19     guard let devSettings = bridge?.module(forName: "DevSettings") as? RCTDevSettings else {
20       return nil
21     }
22 
23     let container = DevMenuItemsContainer()
24 
25     let reload = DevMenuExtensions.reloadAction {
26       // Without this the `expo-splash-screen` will reject
27       // No native splash screen registered for given view controller. Call 'SplashScreen.show' for given view controller first.
28       DevMenuManager.shared.hideMenu();
29       self.bridge?.requestReload()
30     }
31     reload.isAvailable = { !DevMenuExtensions.checkIfLogBoxIsOpened() }
32 
33     let inspector = DevMenuExtensions.elementInspectorAction {
34       devSettings.toggleElementInspector()
35     }
36     inspector.isEnabled = { devSettings.isElementInspectorShown }
37 
38     #if DEBUG
39     let remoteDebug = DevMenuExtensions.remoteDebugAction {
40       DispatchQueue.main.async {
41         devSettings.isDebuggingRemotely = !devSettings.isDebuggingRemotely
42       }
43     }
44     remoteDebug.isAvailable = { devSettings.isRemoteDebuggingAvailable }
45     remoteDebug.isEnabled = { devSettings.isDebuggingRemotely }
46 
47     let fastRefresh = DevMenuExtensions.fastRefreshAction {
48       devSettings.isHotLoadingEnabled = !devSettings.isHotLoadingEnabled
49     }
50     fastRefresh.isAvailable = { devSettings.isHotLoadingAvailable }
51     fastRefresh.isEnabled = { devSettings.isHotLoadingEnabled }
52 
53     let perfMonitor = DevMenuExtensions.performanceMonitorAction {
54       if let perfMonitorModule = self.bridge?.module(forName: "PerfMonitor") as? RCTPerfMonitor {
55         DispatchQueue.main.async {
56           devSettings.isPerfMonitorShown ? perfMonitorModule.hide() : perfMonitorModule.show()
57           devSettings.isPerfMonitorShown = !devSettings.isPerfMonitorShown
58         }
59       }
60     }
61     perfMonitor.isAvailable = { self.bridge?.module(forName: "PerfMonitor") != nil }
62     perfMonitor.isEnabled = { devSettings.isPerfMonitorShown }
63 
64     container.addItem(remoteDebug)
65     container.addItem(fastRefresh)
66     container.addItem(perfMonitor)
67 
68     container.addItem(reload)
69     container.addItem(inspector)
70     #endif
71 
72     return container
73   }
74 
75   @objc
76   open func devMenuScreens(_ settings: DevMenuExtensionSettingsProtocol) -> [DevMenuScreen]? {
77     return nil
78   }
79 
80   @objc
81   open func devMenuDataSources(_ settings: DevMenuExtensionSettingsProtocol) -> [DevMenuDataSourceProtocol]? {
82     return nil
83   }
84 
85   // MARK: static helpers
86 
87   private static func reloadAction(action: @escaping () -> ()) -> DevMenuAction {
88     let reload = DevMenuAction(withId: "reload", action: action)
89     reload.label = { "Reload" }
90     reload.glyphName = { "reload" }
91     reload.importance = DevMenuScreenItem.ImportanceHighest
92     reload.registerKeyCommand(input: "r", modifiers: .command)
93     return reload
94   }
95 
96   private static func elementInspectorAction(_ action: @escaping () -> ()) -> DevMenuAction {
97     let inspector = DevMenuAction(withId: "inspector", action: action)
98     inspector.label = { inspector.isEnabled() ? "Hide Element Inspector" : "Show Element Inspector" }
99     inspector.glyphName = { "border-style" }
100     inspector.importance = DevMenuScreenItem.ImportanceHigh
101     inspector.registerKeyCommand(input: "i", modifiers: .command)
102     return inspector
103   }
104 
105   private static func remoteDebugAction(_ action: @escaping () -> ()) -> DevMenuAction {
106     let remoteDebug = DevMenuAction(withId: "remote-debug", action: action)
107     remoteDebug.label = { remoteDebug.isAvailable() ? remoteDebug.isEnabled() ? "Stop Remote Debugging" : "Debug Remote JS" : "Remote Debugger Unavailable" }
108     remoteDebug.glyphName = { "remote-desktop" }
109     remoteDebug.importance = DevMenuScreenItem.ImportanceLow
110     return remoteDebug
111   }
112 
113   private static func fastRefreshAction(_ action: @escaping () -> ()) -> DevMenuAction {
114     let fastRefresh = DevMenuAction(withId: "fast-refresh", action: action)
115     fastRefresh.label = { fastRefresh.isAvailable() ? fastRefresh.isEnabled() ? "Disable Fast Refresh" : "Enable Fast Refresh" : "Fast Refresh Unavailable" }
116     fastRefresh.glyphName = { "run-fast" }
117     fastRefresh.importance = DevMenuScreenItem.ImportanceLow
118     return fastRefresh
119   }
120 
121   private static func performanceMonitorAction(_ action: @escaping () -> ()) -> DevMenuAction {
122     let perfMonitor = DevMenuAction(withId: "performance-monitor", action: action)
123     perfMonitor.label = { perfMonitor.isAvailable() ? perfMonitor.isEnabled() ? "Hide Performance Monitor" : "Show Performance Monitor" : "Performance Monitor Unavailable" }
124     perfMonitor.glyphName = { "speedometer" }
125     perfMonitor.importance = DevMenuScreenItem.ImportanceHigh
126     perfMonitor.registerKeyCommand(input: "p", modifiers: .command)
127     return perfMonitor
128   }
129 
130   private static func checkIfLogBoxIsOpened() -> Bool {
131     return UIApplication.shared.windows.contains {
132       let className = String(describing: type(of: $0))
133       if className == "RCTLogBoxView" || className == "RCTRedBoxView" {
134         return true
135       }
136 
137       return false
138     }
139   }
140 }
141