expo-dev-menu 55.0.2 → 55.0.3
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.
- package/CHANGELOG.md +17 -0
- package/android/build.gradle +2 -2
- package/android/src/debug/java/expo/modules/devmenu/DevMenuPreferences.kt +2 -2
- package/android/src/debug/java/expo/modules/devmenu/compose/DevMenuState.kt +2 -1
- package/android/src/debug/java/expo/modules/devmenu/compose/ui/DevMenuScreen.kt +4 -4
- package/android/src/debug/java/expo/modules/devmenu/compose/ui/ToolsSection.kt +27 -23
- package/android/src/debug/java/expo/modules/devmenu/websockets/DevMenuCommandHandlersProvider.kt +21 -1
- package/ios/DevMenuManager.swift +85 -4
- package/ios/DevMenuWindow-default.swift +1 -0
- package/ios/EXDevMenuDevSettings.swift +1 -1
- package/ios/FAB/DevMenuFABView.swift +195 -0
- package/ios/FAB/DevMenuFABWindow.swift +105 -0
- package/ios/Modules/DevMenuPreferences.swift +20 -3
- package/ios/SwiftUI/DevMenuAppInfo.swift +1 -0
- package/ios/SwiftUI/DevMenuDeveloperTools.swift +18 -5
- package/ios/SwiftUI/DevMenuMainView.swift +18 -17
- package/ios/SwiftUI/DevMenuOnboardingView.swift +2 -1
- package/ios/SwiftUI/DevMenuRootView.swift +4 -1
- package/ios/SwiftUI/DevMenuViewModel.swift +20 -0
- package/ios/SwiftUI/HeaderView.swift +1 -1
- package/ios/SwiftUI/SourceMapExplorerView.swift +105 -40
- package/ios/SwiftUI/SourceMapService.swift +10 -3
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,23 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 55.0.3 — 2026-01-27
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- [iOS] Add delegate method to control visibility of "Open React Native dev menu" option. ([#42541](https://github.com/expo/expo/pull/42541) by [@alanjhughes](https://github.com/alanjhughes))
|
|
18
|
+
- [iOS] Add action button. ([#42587](https://github.com/expo/expo/pull/42587) by [@alanjhughes](https://github.com/alanjhughes))
|
|
19
|
+
|
|
20
|
+
### 🐛 Bug fixes
|
|
21
|
+
|
|
22
|
+
- [iOS] Fix tvOS compilation errors in Source Map Explorer by excluding tvOS from unavailable SwiftUI APIs (`navigationBarTitleDisplayMode`, `listStyle(.insetGrouped)`, `Menu`) and adding proper availability checks. Refactored platform-specific modifiers to use `@ViewBuilder` instead of `AnyView` for better type preservation and performance. ([#42574](https://github.com/expo/expo/pull/42574) by [@OtavioStasiak](https://github.com/OtavioStasiak))
|
|
23
|
+
- [Android] Fix dev menu not opening when pressing m key in CLI. ([#42566](https://github.com/expo/expo/pull/42566) by [@lukmccall](https://github.com/lukmccall))
|
|
24
|
+
|
|
25
|
+
### 💡 Others
|
|
26
|
+
|
|
27
|
+
- Make the Action Button always enabled on Quest. ([#42562](https://github.com/expo/expo/pull/42562) by [@behenate](https://github.com/behenate))
|
|
28
|
+
- Moves connection info lower in the menu. ([#42568](https://github.com/expo/expo/pull/42568) and [#42569](https://github.com/expo/expo/pull/42569) by [@alanjhughes](https://github.com/alanjhughes))
|
|
29
|
+
|
|
13
30
|
## 55.0.2 — 2026-01-26
|
|
14
31
|
|
|
15
32
|
### 🎉 New features
|
package/android/build.gradle
CHANGED
|
@@ -12,7 +12,7 @@ apply plugin: 'expo-module-gradle-plugin'
|
|
|
12
12
|
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
|
13
13
|
|
|
14
14
|
group = 'host.exp.exponent'
|
|
15
|
-
version = '55.0.
|
|
15
|
+
version = '55.0.3'
|
|
16
16
|
|
|
17
17
|
def hasDevLauncher = findProject(":expo-dev-launcher") != null
|
|
18
18
|
def configureInRelease = findProperty("expo.devmenu.configureInRelease") == "true"
|
|
@@ -29,7 +29,7 @@ android {
|
|
|
29
29
|
|
|
30
30
|
defaultConfig {
|
|
31
31
|
versionCode 10
|
|
32
|
-
versionName '55.0.
|
|
32
|
+
versionName '55.0.3'
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
buildTypes {
|
|
@@ -3,6 +3,7 @@ package expo.modules.devmenu
|
|
|
3
3
|
import android.app.Application
|
|
4
4
|
import android.content.Context.MODE_PRIVATE
|
|
5
5
|
import android.content.SharedPreferences
|
|
6
|
+
import expo.modules.core.utilities.VRUtilities
|
|
6
7
|
import expo.modules.devmenu.helpers.preferences
|
|
7
8
|
|
|
8
9
|
private const val DEV_SETTINGS_PREFERENCES = "expo.modules.devmenu.sharedpreferences"
|
|
@@ -88,7 +89,6 @@ class DevMenuDefaultPreferences(
|
|
|
88
89
|
override var isOnboardingFinished: Boolean
|
|
89
90
|
by preferences(sharedPreferences, false)
|
|
90
91
|
|
|
91
|
-
// TODO: @behenate, on VR this value should be true by default
|
|
92
92
|
override var showFab: Boolean
|
|
93
|
-
by preferences(sharedPreferences,
|
|
93
|
+
by preferences(sharedPreferences, VRUtilities.isQuest())
|
|
94
94
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package expo.modules.devmenu.compose
|
|
2
2
|
|
|
3
|
+
import expo.modules.core.utilities.VRUtilities
|
|
3
4
|
import expo.modules.devmenu.DevToolsSettings
|
|
4
5
|
import org.json.JSONObject
|
|
5
6
|
|
|
@@ -8,7 +9,7 @@ data class DevMenuState(
|
|
|
8
9
|
val isOpen: Boolean = false,
|
|
9
10
|
val devToolsSettings: DevToolsSettings = DevToolsSettings(),
|
|
10
11
|
val isOnboardingFinished: Boolean = false,
|
|
11
|
-
val showFab: Boolean =
|
|
12
|
+
val showFab: Boolean = VRUtilities.isQuest(),
|
|
12
13
|
val customItems: List<CustomItem> = emptyList(),
|
|
13
14
|
val hasGoHomeAction: Boolean = false,
|
|
14
15
|
val isInPictureInPictureMode: Boolean = false
|
|
@@ -38,10 +38,6 @@ fun DevMenuScreen(
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
Column {
|
|
41
|
-
BundlerInfo(bundlerIp = appInfo.hostUrl)
|
|
42
|
-
|
|
43
|
-
Spacer(NewAppTheme.spacing.`2`)
|
|
44
|
-
|
|
45
41
|
Row(
|
|
46
42
|
horizontalArrangement = Arrangement.spacedBy(NewAppTheme.spacing.`2`),
|
|
47
43
|
verticalAlignment = Alignment.CenterVertically
|
|
@@ -84,6 +80,10 @@ fun DevMenuScreen(
|
|
|
84
80
|
}
|
|
85
81
|
}
|
|
86
82
|
|
|
83
|
+
BundlerInfo(bundlerIp = appInfo.hostUrl)
|
|
84
|
+
|
|
85
|
+
Spacer(NewAppTheme.spacing.`5`)
|
|
86
|
+
|
|
87
87
|
SystemSection(
|
|
88
88
|
appInfo.appVersion,
|
|
89
89
|
appInfo.sdkVersion,
|
|
@@ -3,6 +3,7 @@ package expo.modules.devmenu.compose.ui
|
|
|
3
3
|
import androidx.compose.foundation.layout.Column
|
|
4
4
|
import androidx.compose.runtime.Composable
|
|
5
5
|
import androidx.compose.ui.unit.dp
|
|
6
|
+
import expo.modules.core.utilities.VRUtilities
|
|
6
7
|
import expo.modules.devmenu.DevToolsSettings
|
|
7
8
|
import expo.modules.devmenu.compose.DevMenuAction
|
|
8
9
|
import expo.modules.devmenu.compose.DevMenuActionHandler
|
|
@@ -111,30 +112,33 @@ fun ToolsSection(
|
|
|
111
112
|
// }
|
|
112
113
|
// )
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
// Hide FAB toggle on Quest devices since FAB is always on there
|
|
116
|
+
if (!VRUtilities.isQuest()) {
|
|
117
|
+
Divider(thickness = 0.5.dp)
|
|
115
118
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
119
|
+
NewMenuButton(
|
|
120
|
+
withSurface = false,
|
|
121
|
+
icon = {
|
|
122
|
+
MenuIcons.Fab(
|
|
123
|
+
size = 20.dp,
|
|
124
|
+
tint = NewAppTheme.colors.icon.tertiary
|
|
125
|
+
)
|
|
126
|
+
},
|
|
127
|
+
content = {
|
|
128
|
+
NewText(
|
|
129
|
+
text = "Action button"
|
|
130
|
+
)
|
|
131
|
+
},
|
|
132
|
+
rightComponent = {
|
|
133
|
+
ToggleSwitch(
|
|
134
|
+
isToggled = showFab
|
|
135
|
+
)
|
|
136
|
+
},
|
|
137
|
+
onClick = {
|
|
138
|
+
onAction(DevMenuAction.ToggleFab)
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
}
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
}
|
package/android/src/debug/java/expo/modules/devmenu/websockets/DevMenuCommandHandlersProvider.kt
CHANGED
|
@@ -6,6 +6,10 @@ import com.facebook.react.packagerconnection.NotificationOnlyHandler
|
|
|
6
6
|
import expo.modules.devmenu.devtools.DevMenuDevToolsDelegate
|
|
7
7
|
import org.json.JSONObject
|
|
8
8
|
import java.lang.ref.WeakReference
|
|
9
|
+
import java.util.Date
|
|
10
|
+
|
|
11
|
+
private object Mutex
|
|
12
|
+
private var lastMessage = 0L
|
|
9
13
|
|
|
10
14
|
class DevMenuCommandHandlersProvider(
|
|
11
15
|
weakDevSupportManager: WeakReference<out DevSupportManager>
|
|
@@ -32,6 +36,8 @@ class DevMenuCommandHandlersProvider(
|
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
fun createCommandHandlers(): Map<String, NotificationOnlyHandler> {
|
|
39
|
+
lastMessage = Date().time
|
|
40
|
+
|
|
35
41
|
return mapOf(
|
|
36
42
|
"reload" to onReload,
|
|
37
43
|
"devMenu" to onDevMenu,
|
|
@@ -42,7 +48,21 @@ class DevMenuCommandHandlersProvider(
|
|
|
42
48
|
private fun createHandler(action: (params: Any?) -> Unit): NotificationOnlyHandler {
|
|
43
49
|
return object : NotificationOnlyHandler() {
|
|
44
50
|
override fun onNotification(params: Any?) {
|
|
45
|
-
|
|
51
|
+
val currentTime = Date().time
|
|
52
|
+
|
|
53
|
+
synchronized(Mutex) {
|
|
54
|
+
val diff = currentTime - lastMessage
|
|
55
|
+
if (diff < 100) {
|
|
56
|
+
Log.w(
|
|
57
|
+
"DevMenu",
|
|
58
|
+
"Throttling incoming dev menu command. Time since last command: ${diff}ms"
|
|
59
|
+
)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
action(params)
|
|
64
|
+
lastMessage = currentTime
|
|
65
|
+
}
|
|
46
66
|
}
|
|
47
67
|
}
|
|
48
68
|
}
|
package/ios/DevMenuManager.swift
CHANGED
|
@@ -7,6 +7,27 @@ import CoreGraphics
|
|
|
7
7
|
import CoreMedia
|
|
8
8
|
import Combine
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
Configuration options for customizing the dev menu appearance.
|
|
12
|
+
Host apps (e.g., Expo Go) can set these to tailor the menu for their context.
|
|
13
|
+
The defaults match the standard dev menu behavior.
|
|
14
|
+
*/
|
|
15
|
+
@objc
|
|
16
|
+
public class DevMenuConfiguration: NSObject {
|
|
17
|
+
/// Whether to show the debugging tip (e.g., "Debugging not working? Try manually reloading first.")
|
|
18
|
+
@objc public var showDebuggingTip: Bool = true
|
|
19
|
+
|
|
20
|
+
/// Whether to show the "Connected to:" host URL section
|
|
21
|
+
@objc public var showHostUrl: Bool = true
|
|
22
|
+
|
|
23
|
+
/// Whether to show the Fast Refresh toggle
|
|
24
|
+
@objc public var showFastRefresh: Bool = true
|
|
25
|
+
|
|
26
|
+
/// Custom title for the onboarding text. Use to replace "development builds" with e.g. "Expo Go".
|
|
27
|
+
/// When nil, the default "development builds" text is used.
|
|
28
|
+
@objc public var onboardingAppName: String?
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
class Dispatch {
|
|
11
32
|
static func mainSync<T>(_ closure: () -> T) -> T {
|
|
12
33
|
if Thread.isMainThread {
|
|
@@ -51,6 +72,8 @@ open class DevMenuManager: NSObject {
|
|
|
51
72
|
var packagerConnectionHandler: DevMenuPackagerConnectionHandler?
|
|
52
73
|
var canLaunchDevMenuOnStart = true
|
|
53
74
|
|
|
75
|
+
@objc public var configuration = DevMenuConfiguration()
|
|
76
|
+
|
|
54
77
|
static public var wasInitilized = false
|
|
55
78
|
|
|
56
79
|
/**
|
|
@@ -67,6 +90,13 @@ open class DevMenuManager: NSObject {
|
|
|
67
90
|
*/
|
|
68
91
|
var window: DevMenuWindow?
|
|
69
92
|
|
|
93
|
+
#if !os(macOS) && !os(tvOS)
|
|
94
|
+
/**
|
|
95
|
+
The window that hosts the floating action button.
|
|
96
|
+
*/
|
|
97
|
+
var fabWindow: DevMenuFABWindow?
|
|
98
|
+
#endif
|
|
99
|
+
|
|
70
100
|
var currentScreen: String?
|
|
71
101
|
|
|
72
102
|
weak var hostDelegate: DevMenuHostDelegate?
|
|
@@ -79,7 +109,10 @@ open class DevMenuManager: NSObject {
|
|
|
79
109
|
if let currentBridge {
|
|
80
110
|
DispatchQueue.main.async {
|
|
81
111
|
self.disableRNDevMenuHoykeys(for: currentBridge)
|
|
112
|
+
self.updateFABVisibility()
|
|
82
113
|
}
|
|
114
|
+
} else {
|
|
115
|
+
updateFABVisibility()
|
|
83
116
|
}
|
|
84
117
|
}
|
|
85
118
|
}
|
|
@@ -250,7 +283,11 @@ open class DevMenuManager: NSObject {
|
|
|
250
283
|
@objc
|
|
251
284
|
@discardableResult
|
|
252
285
|
public func hideMenu() -> Bool {
|
|
253
|
-
|
|
286
|
+
let result = setVisibility(false)
|
|
287
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
|
288
|
+
self?.updateFABVisibility()
|
|
289
|
+
}
|
|
290
|
+
return result
|
|
254
291
|
}
|
|
255
292
|
|
|
256
293
|
/**
|
|
@@ -270,6 +307,15 @@ open class DevMenuManager: NSObject {
|
|
|
270
307
|
return delegate.responds(to: #selector(DevMenuHostDelegate.devMenuNavigateHome))
|
|
271
308
|
}
|
|
272
309
|
|
|
310
|
+
@objc
|
|
311
|
+
public var shouldShowReactNativeDevMenu: Bool {
|
|
312
|
+
guard let delegate = hostDelegate,
|
|
313
|
+
delegate.responds(to: #selector(DevMenuHostDelegate.devMenuShouldShowReactNativeDevMenu)) else {
|
|
314
|
+
return true
|
|
315
|
+
}
|
|
316
|
+
return delegate.devMenuShouldShowReactNativeDevMenu?() ?? true
|
|
317
|
+
}
|
|
318
|
+
|
|
273
319
|
@objc
|
|
274
320
|
public func navigateHome() {
|
|
275
321
|
guard let delegate = hostDelegate,
|
|
@@ -369,9 +415,10 @@ open class DevMenuManager: NSObject {
|
|
|
369
415
|
#if os(macOS)
|
|
370
416
|
self.window?.makeKeyAndOrderFront(nil)
|
|
371
417
|
#else
|
|
418
|
+
self.updateFABVisibility()
|
|
419
|
+
|
|
372
420
|
if self.window?.windowScene == nil {
|
|
373
|
-
let
|
|
374
|
-
let windowScene = keyWindowScene ?? UIApplication.shared.connectedScenes
|
|
421
|
+
let windowScene = UIApplication.shared.connectedScenes
|
|
375
422
|
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene
|
|
376
423
|
self.window?.windowScene = windowScene
|
|
377
424
|
}
|
|
@@ -379,7 +426,12 @@ open class DevMenuManager: NSObject {
|
|
|
379
426
|
#endif
|
|
380
427
|
}
|
|
381
428
|
} else {
|
|
382
|
-
DispatchQueue.main.async {
|
|
429
|
+
DispatchQueue.main.async {
|
|
430
|
+
self.window?.closeBottomSheet(nil)
|
|
431
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
432
|
+
self.updateFABVisibility()
|
|
433
|
+
}
|
|
434
|
+
}
|
|
383
435
|
}
|
|
384
436
|
return true
|
|
385
437
|
}
|
|
@@ -457,4 +509,33 @@ open class DevMenuManager: NSObject {
|
|
|
457
509
|
let devToolsDelegate = getDevToolsDelegate()
|
|
458
510
|
devToolsDelegate?.toggleFastRefresh()
|
|
459
511
|
}
|
|
512
|
+
|
|
513
|
+
#if !os(macOS) && !os(tvOS)
|
|
514
|
+
private func setupFABWindowIfNeeded(for windowScene: UIWindowScene) {
|
|
515
|
+
guard fabWindow == nil else { return }
|
|
516
|
+
fabWindow = DevMenuFABWindow(manager: self, windowScene: windowScene)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
@objc
|
|
520
|
+
public func updateFABVisibility() {
|
|
521
|
+
DispatchQueue.main.async { [weak self] in
|
|
522
|
+
guard let self = self else { return }
|
|
523
|
+
|
|
524
|
+
if self.fabWindow == nil {
|
|
525
|
+
if let windowScene = UIApplication.shared.connectedScenes
|
|
526
|
+
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
|
|
527
|
+
self.setupFABWindowIfNeeded(for: windowScene)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
let shouldShow = DevMenuPreferences.showFloatingActionButton && !self.isVisible && self.currentBridge != nil
|
|
532
|
+
self.fabWindow?.setVisible(shouldShow, animated: true)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
#else
|
|
536
|
+
@objc
|
|
537
|
+
public func updateFABVisibility() {
|
|
538
|
+
// FAB not available on macOS/tvOS
|
|
539
|
+
}
|
|
540
|
+
#endif
|
|
460
541
|
}
|
|
@@ -31,7 +31,7 @@ class EXDevMenuDevSettings: NSObject {
|
|
|
31
31
|
devSettings["isHotLoadingEnabled"] = bridgeSettings.isHotLoadingEnabled
|
|
32
32
|
devSettings["isPerfMonitorShown"] = bridgeSettings.isPerfMonitorShown
|
|
33
33
|
devSettings["isHotLoadingAvailable"] = bridgeSettings.isHotLoadingAvailable
|
|
34
|
-
devSettings["isPerfMonitorAvailable"] = isPerfMonitorAvailable
|
|
34
|
+
devSettings["isPerfMonitorAvailable"] = isPerfMonitorAvailable && manager.currentManifest?.isDevelopmentMode() == true
|
|
35
35
|
devSettings["isJSInspectorAvailable"] = bridgeSettings.isDeviceDebuggingAvailable
|
|
36
36
|
|
|
37
37
|
let isElementInspectorAvailable = manager.currentManifest?.isDevelopmentMode()
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
#if !os(macOS) && !os(tvOS)
|
|
4
|
+
|
|
5
|
+
import SwiftUI
|
|
6
|
+
|
|
7
|
+
enum FABConstants {
|
|
8
|
+
static let iconSize: CGFloat = 44
|
|
9
|
+
static let margin: CGFloat = 16
|
|
10
|
+
static let dragThreshold: CGFloat = 40
|
|
11
|
+
static let momentumFactor: CGFloat = 0.35
|
|
12
|
+
static let labelDismissDelay: TimeInterval = 10
|
|
13
|
+
|
|
14
|
+
static let snapAnimation: Animation = .spring(
|
|
15
|
+
response: 0.6,
|
|
16
|
+
dampingFraction: 0.7,
|
|
17
|
+
blendDuration: 0
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
struct FabPill: View {
|
|
22
|
+
@Binding var isPressed: Bool
|
|
23
|
+
@State private var showLabel = true
|
|
24
|
+
|
|
25
|
+
var body: some View {
|
|
26
|
+
VStack(spacing: 4) {
|
|
27
|
+
Image(systemName: "gearshape.fill")
|
|
28
|
+
.font(.system(size: 20))
|
|
29
|
+
.foregroundStyle(.white)
|
|
30
|
+
.frame(width: FABConstants.iconSize, height: FABConstants.iconSize)
|
|
31
|
+
.background(Color.blue, in: Circle())
|
|
32
|
+
.overlay(
|
|
33
|
+
Circle()
|
|
34
|
+
.stroke(Color.blue.opacity(0.5), lineWidth: 4)
|
|
35
|
+
.frame(width: FABConstants.iconSize + 4, height: FABConstants.iconSize + 4)
|
|
36
|
+
)
|
|
37
|
+
.opacity(isPressed ? 0.8 : 1.0)
|
|
38
|
+
.scaleEffect(isPressed ? 0.9 : 1.0)
|
|
39
|
+
.animation(.easeInOut(duration: 0.1), value: isPressed)
|
|
40
|
+
|
|
41
|
+
if showLabel {
|
|
42
|
+
Text("Dev tools")
|
|
43
|
+
.font(.system(size: 11, weight: .medium))
|
|
44
|
+
.foregroundStyle(.secondary)
|
|
45
|
+
.fixedSize()
|
|
46
|
+
.padding(.horizontal, 8)
|
|
47
|
+
.padding(.vertical, 3)
|
|
48
|
+
.background(.regularMaterial, in: Capsule())
|
|
49
|
+
.transition(.opacity.combined(with: .scale(scale: 0.8)))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
.task {
|
|
53
|
+
try? await Task.sleep(nanoseconds: UInt64(1_000_000_000 * FABConstants.labelDismissDelay))
|
|
54
|
+
await MainActor.run {
|
|
55
|
+
withAnimation(.easeOut(duration: 0.3)) {
|
|
56
|
+
showLabel = false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
struct DevMenuFABView: View {
|
|
64
|
+
let onOpenMenu: () -> Void
|
|
65
|
+
let onFrameChange: (CGRect) -> Void
|
|
66
|
+
|
|
67
|
+
private let fabSize = CGSize(width: 72, height: FABConstants.iconSize + 24)
|
|
68
|
+
|
|
69
|
+
@State private var position: CGPoint = .zero
|
|
70
|
+
@State private var isDragging = false
|
|
71
|
+
@State private var isPressed = false
|
|
72
|
+
@State private var dragStartPosition: CGPoint = .zero
|
|
73
|
+
|
|
74
|
+
private var currentFrame: CGRect {
|
|
75
|
+
CGRect(origin: position, size: fabSize)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
var body: some View {
|
|
79
|
+
GeometryReader { geometry in
|
|
80
|
+
let safeArea = geometry.safeAreaInsets
|
|
81
|
+
FabPill(isPressed: $isPressed)
|
|
82
|
+
.frame(width: fabSize.width, height: fabSize.height)
|
|
83
|
+
.position(x: currentFrame.midX, y: currentFrame.midY)
|
|
84
|
+
.gesture(dragGesture(bounds: geometry.size, safeArea: safeArea))
|
|
85
|
+
.onAppear {
|
|
86
|
+
let initialPos = defaultPosition(bounds: geometry.size, safeArea: safeArea)
|
|
87
|
+
position = initialPos
|
|
88
|
+
onFrameChange(CGRect(origin: initialPos, size: fabSize))
|
|
89
|
+
}
|
|
90
|
+
.onChange(of: geometry.size) { newSize in
|
|
91
|
+
let newPos = snapToEdge(
|
|
92
|
+
from: position,
|
|
93
|
+
velocity: .zero,
|
|
94
|
+
bounds: newSize,
|
|
95
|
+
safeArea: safeArea
|
|
96
|
+
)
|
|
97
|
+
position = newPos
|
|
98
|
+
onFrameChange(CGRect(origin: newPos, size: fabSize))
|
|
99
|
+
}
|
|
100
|
+
.animation(isDragging ? nil : FABConstants.snapAnimation, value: position)
|
|
101
|
+
}
|
|
102
|
+
.ignoresSafeArea()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private func dragGesture(bounds: CGSize, safeArea: EdgeInsets) -> some Gesture {
|
|
106
|
+
DragGesture(minimumDistance: 0)
|
|
107
|
+
.onChanged { value in
|
|
108
|
+
if !isPressed {
|
|
109
|
+
isPressed = true
|
|
110
|
+
}
|
|
111
|
+
if !isDragging && value.translation.magnitude > FABConstants.dragThreshold {
|
|
112
|
+
isDragging = true
|
|
113
|
+
isPressed = false
|
|
114
|
+
dragStartPosition = position
|
|
115
|
+
}
|
|
116
|
+
if isDragging {
|
|
117
|
+
position = CGPoint(
|
|
118
|
+
x: dragStartPosition.x + value.translation.width,
|
|
119
|
+
y: dragStartPosition.y + value.translation.height
|
|
120
|
+
)
|
|
121
|
+
onFrameChange(currentFrame)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
.onEnded { value in
|
|
125
|
+
isPressed = false
|
|
126
|
+
let dragDistance = value.translation.magnitude
|
|
127
|
+
|
|
128
|
+
if dragDistance < FABConstants.dragThreshold {
|
|
129
|
+
isDragging = false
|
|
130
|
+
onOpenMenu()
|
|
131
|
+
} else {
|
|
132
|
+
let velocity = CGPoint(
|
|
133
|
+
x: value.predictedEndLocation.x - value.location.x,
|
|
134
|
+
y: value.predictedEndLocation.y - value.location.y
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
let newPos = snapToEdge(
|
|
138
|
+
from: position,
|
|
139
|
+
velocity: velocity,
|
|
140
|
+
bounds: bounds,
|
|
141
|
+
safeArea: safeArea
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
DispatchQueue.main.async {
|
|
145
|
+
isDragging = false
|
|
146
|
+
position = newPos
|
|
147
|
+
onFrameChange(CGRect(origin: newPos, size: fabSize))
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private func defaultPosition(bounds: CGSize, safeArea: EdgeInsets) -> CGPoint {
|
|
154
|
+
CGPoint(
|
|
155
|
+
x: bounds.width - fabSize.width - FABConstants.margin,
|
|
156
|
+
y: (bounds.height / 2) - fabSize.height
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private func snapToEdge(
|
|
161
|
+
from point: CGPoint,
|
|
162
|
+
velocity: CGPoint,
|
|
163
|
+
bounds: CGSize,
|
|
164
|
+
safeArea: EdgeInsets
|
|
165
|
+
) -> CGPoint {
|
|
166
|
+
let margin = FABConstants.margin
|
|
167
|
+
let momentumX = velocity.x * FABConstants.momentumFactor
|
|
168
|
+
let momentumY = velocity.y * FABConstants.momentumFactor
|
|
169
|
+
|
|
170
|
+
let estimatedCenterX = point.x + self.fabSize.width / 2 + momentumX
|
|
171
|
+
let targetX: CGFloat = estimatedCenterX < bounds.width / 2
|
|
172
|
+
? margin
|
|
173
|
+
: bounds.width - self.fabSize.width - margin
|
|
174
|
+
|
|
175
|
+
let minY = margin + safeArea.top
|
|
176
|
+
let maxY = bounds.height - self.fabSize.height - margin - safeArea.bottom
|
|
177
|
+
let targetY = (point.y + momentumY).clamped(to: minY...maxY)
|
|
178
|
+
|
|
179
|
+
return CGPoint(x: targetX, y: targetY)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private extension CGSize {
|
|
184
|
+
var magnitude: CGFloat {
|
|
185
|
+
sqrt(width * width + height * height)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private extension Comparable {
|
|
190
|
+
func clamped(to range: ClosedRange<Self>) -> Self {
|
|
191
|
+
min(max(self, range.lowerBound), range.upperBound)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#endif
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
#if !os(macOS) && !os(tvOS)
|
|
4
|
+
|
|
5
|
+
import UIKit
|
|
6
|
+
import SwiftUI
|
|
7
|
+
|
|
8
|
+
/// A passthrough window that hosts the floating action button.
|
|
9
|
+
class DevMenuFABWindow: UIWindow {
|
|
10
|
+
private weak var manager: DevMenuManager?
|
|
11
|
+
private var hostingController: UIHostingController<DevMenuFABView>?
|
|
12
|
+
var fabFrame: CGRect = .zero
|
|
13
|
+
|
|
14
|
+
init(manager: DevMenuManager, windowScene: UIWindowScene) {
|
|
15
|
+
self.manager = manager
|
|
16
|
+
super.init(windowScene: windowScene)
|
|
17
|
+
setupWindow()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@available(*, unavailable)
|
|
21
|
+
required init?(coder: NSCoder) {
|
|
22
|
+
fatalError("init(coder:) has not been implemented")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private func setupWindow() {
|
|
26
|
+
windowLevel = UIWindow.Level.statusBar - 1
|
|
27
|
+
backgroundColor = .clear
|
|
28
|
+
isHidden = true
|
|
29
|
+
alpha = 0
|
|
30
|
+
|
|
31
|
+
let fabView = DevMenuFABView(
|
|
32
|
+
onOpenMenu: { [weak self] in
|
|
33
|
+
self?.manager?.openMenu()
|
|
34
|
+
},
|
|
35
|
+
onFrameChange: { [weak self] frame in
|
|
36
|
+
self?.fabFrame = frame
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
let hostingController = UIHostingController(rootView: fabView)
|
|
41
|
+
hostingController.view.backgroundColor = .clear
|
|
42
|
+
self.hostingController = hostingController
|
|
43
|
+
rootViewController = hostingController
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private var edgeTranslation: CGAffineTransform {
|
|
47
|
+
let screenWidth = windowScene?.screen.bounds.width ?? UIScreen.main.bounds.width
|
|
48
|
+
let isOnRight = fabFrame == .zero || fabFrame.midX > (screenWidth / 2)
|
|
49
|
+
let dx: CGFloat = isOnRight ? 60 : -60
|
|
50
|
+
return CGAffineTransform(translationX: dx, y: 0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func setVisible(_ visible: Bool, animated: Bool = true) {
|
|
54
|
+
if visible {
|
|
55
|
+
isHidden = false
|
|
56
|
+
if animated {
|
|
57
|
+
alpha = 0
|
|
58
|
+
transform = edgeTranslation
|
|
59
|
+
UIView.animate(
|
|
60
|
+
withDuration: 0.5,
|
|
61
|
+
delay: 0,
|
|
62
|
+
usingSpringWithDamping: 0.6,
|
|
63
|
+
initialSpringVelocity: 0.8,
|
|
64
|
+
options: .curveEaseOut
|
|
65
|
+
) {
|
|
66
|
+
self.alpha = 1
|
|
67
|
+
self.transform = .identity
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
alpha = 1
|
|
71
|
+
transform = .identity
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
if animated {
|
|
75
|
+
UIView.animate(
|
|
76
|
+
withDuration: 0.4,
|
|
77
|
+
delay: 0,
|
|
78
|
+
usingSpringWithDamping: 0.6,
|
|
79
|
+
initialSpringVelocity: 0.8,
|
|
80
|
+
options: .curveEaseIn
|
|
81
|
+
) {
|
|
82
|
+
self.alpha = 0
|
|
83
|
+
self.transform = self.edgeTranslation
|
|
84
|
+
} completion: { _ in
|
|
85
|
+
self.isHidden = true
|
|
86
|
+
self.transform = .identity
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
alpha = 0
|
|
90
|
+
isHidden = true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
96
|
+
let hitArea = fabFrame.insetBy(dx: -10, dy: -10)
|
|
97
|
+
if hitArea.contains(point) {
|
|
98
|
+
return super.hitTest(point, with: event)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return nil
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#endif
|
|
@@ -6,6 +6,7 @@ let touchGestureEnabledKey = "EXDevMenuTouchGestureEnabled"
|
|
|
6
6
|
let keyCommandsEnabledKey = "EXDevMenuKeyCommandsEnabled"
|
|
7
7
|
let showsAtLaunchKey = "EXDevMenuShowsAtLaunch"
|
|
8
8
|
let isOnboardingFinishedKey = "EXDevMenuIsOnboardingFinished"
|
|
9
|
+
let showFloatingActionButtonKey = "EXDevMenuShowFloatingActionButton"
|
|
9
10
|
|
|
10
11
|
public class DevMenuPreferences: Module {
|
|
11
12
|
public func definition() -> ModuleDefinition {
|
|
@@ -31,7 +32,8 @@ public class DevMenuPreferences: Module {
|
|
|
31
32
|
touchGestureEnabledKey: false,
|
|
32
33
|
keyCommandsEnabledKey: true,
|
|
33
34
|
showsAtLaunchKey: false,
|
|
34
|
-
isOnboardingFinishedKey: true
|
|
35
|
+
isOnboardingFinishedKey: true,
|
|
36
|
+
showFloatingActionButtonKey: false
|
|
35
37
|
])
|
|
36
38
|
#else
|
|
37
39
|
UserDefaults.standard.register(defaults: [
|
|
@@ -39,7 +41,8 @@ public class DevMenuPreferences: Module {
|
|
|
39
41
|
touchGestureEnabledKey: true,
|
|
40
42
|
keyCommandsEnabledKey: true,
|
|
41
43
|
showsAtLaunchKey: false,
|
|
42
|
-
isOnboardingFinishedKey: false
|
|
44
|
+
isOnboardingFinishedKey: false,
|
|
45
|
+
showFloatingActionButtonKey: false
|
|
43
46
|
])
|
|
44
47
|
#endif
|
|
45
48
|
|
|
@@ -129,6 +132,16 @@ public class DevMenuPreferences: Module {
|
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
134
|
|
|
135
|
+
public static var showFloatingActionButton: Bool {
|
|
136
|
+
get {
|
|
137
|
+
return boolForKey(showFloatingActionButtonKey)
|
|
138
|
+
}
|
|
139
|
+
set {
|
|
140
|
+
setBool(newValue, forKey: showFloatingActionButtonKey)
|
|
141
|
+
DevMenuManager.shared.updateFABVisibility()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
132
145
|
/**
|
|
133
146
|
Serializes settings into a dictionary so they can be passed through the bridge.
|
|
134
147
|
*/
|
|
@@ -138,7 +151,8 @@ public class DevMenuPreferences: Module {
|
|
|
138
151
|
"touchGestureEnabled": DevMenuPreferences.touchGestureEnabled,
|
|
139
152
|
"keyCommandsEnabled": DevMenuPreferences.keyCommandsEnabled,
|
|
140
153
|
"showsAtLaunch": DevMenuPreferences.showsAtLaunch,
|
|
141
|
-
"isOnboardingFinished": DevMenuPreferences.isOnboardingFinished
|
|
154
|
+
"isOnboardingFinished": DevMenuPreferences.isOnboardingFinished,
|
|
155
|
+
"showFloatingActionButton": DevMenuPreferences.showFloatingActionButton
|
|
142
156
|
]
|
|
143
157
|
}
|
|
144
158
|
|
|
@@ -155,6 +169,9 @@ public class DevMenuPreferences: Module {
|
|
|
155
169
|
if let showsAtLaunch = settings["showsAtLaunch"] as? Bool {
|
|
156
170
|
DevMenuPreferences.showsAtLaunch = showsAtLaunch
|
|
157
171
|
}
|
|
172
|
+
if let showFloatingActionButton = settings["showFloatingActionButton"] as? Bool {
|
|
173
|
+
DevMenuPreferences.showFloatingActionButton = showFloatingActionButton
|
|
174
|
+
}
|
|
158
175
|
}
|
|
159
176
|
|
|
160
177
|
@objc static func toggleMenu() {
|
|
@@ -38,15 +38,28 @@ struct DevMenuDeveloperTools: View {
|
|
|
38
38
|
)
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
if viewModel.configuration.showFastRefresh {
|
|
42
|
+
Divider()
|
|
43
|
+
|
|
44
|
+
DevMenuToggleButton(
|
|
45
|
+
title: "Fast refresh",
|
|
46
|
+
icon: "figure.run",
|
|
47
|
+
isEnabled: viewModel.devSettings?.isHotLoadingEnabled ?? false,
|
|
48
|
+
action: viewModel.toggleFastRefresh,
|
|
49
|
+
disabled: !(viewModel.devSettings?.isHotLoadingAvailable ?? true)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#if !os(tvOS) && !os(macOS)
|
|
41
54
|
Divider()
|
|
42
55
|
|
|
43
56
|
DevMenuToggleButton(
|
|
44
|
-
title: "
|
|
45
|
-
icon: "
|
|
46
|
-
isEnabled: viewModel.
|
|
47
|
-
action: viewModel.
|
|
48
|
-
disabled: !(viewModel.devSettings?.isHotLoadingAvailable ?? true)
|
|
57
|
+
title: "Action button",
|
|
58
|
+
icon: "hand.tap",
|
|
59
|
+
isEnabled: viewModel.showFloatingActionButton,
|
|
60
|
+
action: viewModel.toggleFloatingActionButton
|
|
49
61
|
)
|
|
62
|
+
#endif
|
|
50
63
|
|
|
51
64
|
Divider()
|
|
52
65
|
|
|
@@ -6,21 +6,11 @@ struct DevMenuMainView: View {
|
|
|
6
6
|
var body: some View {
|
|
7
7
|
ScrollView {
|
|
8
8
|
VStack(spacing: 32) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
copiedMessage: viewModel.hostUrlCopiedMessage
|
|
15
|
-
)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
DevMenuActions(
|
|
19
|
-
canNavigateHome: viewModel.canNavigateHome,
|
|
20
|
-
onReload: viewModel.reload,
|
|
21
|
-
onGoHome: viewModel.goHome
|
|
22
|
-
)
|
|
23
|
-
}
|
|
9
|
+
DevMenuActions(
|
|
10
|
+
canNavigateHome: viewModel.canNavigateHome,
|
|
11
|
+
onReload: viewModel.reload,
|
|
12
|
+
onGoHome: viewModel.goHome
|
|
13
|
+
)
|
|
24
14
|
|
|
25
15
|
if !viewModel.registeredCallbacks.isEmpty {
|
|
26
16
|
CustomItems(
|
|
@@ -31,13 +21,23 @@ struct DevMenuMainView: View {
|
|
|
31
21
|
|
|
32
22
|
DevMenuDeveloperTools()
|
|
33
23
|
|
|
34
|
-
if viewModel.appInfo?.engine == "Hermes" {
|
|
24
|
+
if viewModel.configuration.showDebuggingTip && viewModel.appInfo?.engine == "Hermes" {
|
|
35
25
|
HermesDebuggerTip()
|
|
36
26
|
}
|
|
37
27
|
|
|
28
|
+
if viewModel.configuration.showHostUrl, let hostUrl = viewModel.appInfo?.hostUrl {
|
|
29
|
+
HostUrl(
|
|
30
|
+
hostUrl: hostUrl,
|
|
31
|
+
onCopy: viewModel.copyToClipboard,
|
|
32
|
+
copiedMessage: viewModel.hostUrlCopiedMessage
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
38
36
|
DevMenuAppInfo()
|
|
39
37
|
|
|
40
|
-
|
|
38
|
+
if viewModel.shouldShowReactNativeDevMenu {
|
|
39
|
+
DevMenuRNDevMenu(onOpenRNDevMenu: viewModel.openRNDevMenu)
|
|
40
|
+
}
|
|
41
41
|
}
|
|
42
42
|
.padding()
|
|
43
43
|
}
|
|
@@ -45,6 +45,7 @@ struct DevMenuMainView: View {
|
|
|
45
45
|
.navigationTitle("Dev Menu")
|
|
46
46
|
#if !os(macOS)
|
|
47
47
|
.navigationBarHidden(true)
|
|
48
|
+
.navigationBarTitleDisplayMode(.inline)
|
|
48
49
|
#endif
|
|
49
50
|
}
|
|
50
51
|
}
|
|
@@ -2,6 +2,7 @@ import SwiftUI
|
|
|
2
2
|
|
|
3
3
|
struct DevMenuOnboardingView: View {
|
|
4
4
|
let onFinish: () -> Void
|
|
5
|
+
var appName: String = "development builds"
|
|
5
6
|
@State private var isVisible = true
|
|
6
7
|
|
|
7
8
|
var body: some View {
|
|
@@ -16,7 +17,7 @@ struct DevMenuOnboardingView: View {
|
|
|
16
17
|
private var onboardingOverlay: some View {
|
|
17
18
|
VStack(spacing: 24) {
|
|
18
19
|
VStack(spacing: 16) {
|
|
19
|
-
Text("This is the developer menu. It gives you access to useful tools in
|
|
20
|
+
Text("This is the developer menu. It gives you access to useful tools in \(appName).")
|
|
20
21
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
21
22
|
.font(.callout)
|
|
22
23
|
|
|
@@ -17,7 +17,10 @@ struct DevMenuRootView: View {
|
|
|
17
17
|
|
|
18
18
|
#if !os(tvOS)
|
|
19
19
|
if !viewModel.isOnboardingFinished {
|
|
20
|
-
DevMenuOnboardingView(
|
|
20
|
+
DevMenuOnboardingView(
|
|
21
|
+
onFinish: viewModel.finishOnboarding,
|
|
22
|
+
appName: viewModel.configuration.onboardingAppName ?? "your development builds"
|
|
23
|
+
)
|
|
21
24
|
}
|
|
22
25
|
#endif
|
|
23
26
|
}
|
|
@@ -12,6 +12,7 @@ class DevMenuViewModel: ObservableObject {
|
|
|
12
12
|
@Published var clipboardMessage: String?
|
|
13
13
|
@Published var hostUrlCopiedMessage: String?
|
|
14
14
|
@Published var isOnboardingFinished: Bool = true
|
|
15
|
+
@Published var showFloatingActionButton: Bool = false
|
|
15
16
|
|
|
16
17
|
private let devMenuManager = DevMenuManager.shared
|
|
17
18
|
private var cancellables = Set<AnyCancellable>()
|
|
@@ -27,6 +28,7 @@ class DevMenuViewModel: ObservableObject {
|
|
|
27
28
|
loadAppInfo()
|
|
28
29
|
loadDevSettings()
|
|
29
30
|
loadRegisteredCallbacks()
|
|
31
|
+
loadFloatingActionButtonState()
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
private func loadAppInfo() {
|
|
@@ -165,6 +167,14 @@ class DevMenuViewModel: ObservableObject {
|
|
|
165
167
|
return devMenuManager.canNavigateHome
|
|
166
168
|
}
|
|
167
169
|
|
|
170
|
+
var shouldShowReactNativeDevMenu: Bool {
|
|
171
|
+
return devMenuManager.shouldShowReactNativeDevMenu
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
var configuration: DevMenuConfiguration {
|
|
175
|
+
return devMenuManager.configuration
|
|
176
|
+
}
|
|
177
|
+
|
|
168
178
|
private func checkOnboardingStatus() {
|
|
169
179
|
isOnboardingFinished = devMenuManager.isOnboardingFinished
|
|
170
180
|
}
|
|
@@ -174,6 +184,15 @@ class DevMenuViewModel: ObservableObject {
|
|
|
174
184
|
isOnboardingFinished = true
|
|
175
185
|
}
|
|
176
186
|
|
|
187
|
+
private func loadFloatingActionButtonState() {
|
|
188
|
+
showFloatingActionButton = DevMenuPreferences.showFloatingActionButton
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
func toggleFloatingActionButton() {
|
|
192
|
+
showFloatingActionButton.toggle()
|
|
193
|
+
DevMenuPreferences.showFloatingActionButton = showFloatingActionButton
|
|
194
|
+
}
|
|
195
|
+
|
|
177
196
|
private func observeRegisteredCallbacks() {
|
|
178
197
|
devMenuManager.callbacksPublisher
|
|
179
198
|
.map { $0.map { $0.name } }
|
|
@@ -186,6 +205,7 @@ class DevMenuViewModel: ObservableObject {
|
|
|
186
205
|
.receive(on: DispatchQueue.main)
|
|
187
206
|
.sink { [weak self] _ in
|
|
188
207
|
self?.loadAppInfo()
|
|
208
|
+
self?.loadDevSettings()
|
|
189
209
|
}
|
|
190
210
|
.store(in: &cancellables)
|
|
191
211
|
}
|
|
@@ -11,7 +11,7 @@ typealias PlatformImage = UIImage
|
|
|
11
11
|
|
|
12
12
|
struct HeaderView: View {
|
|
13
13
|
@EnvironmentObject var viewModel: DevMenuViewModel
|
|
14
|
-
@State private var appIcon: PlatformImage?
|
|
14
|
+
@State private var appIcon: PlatformImage?
|
|
15
15
|
|
|
16
16
|
var body: some View {
|
|
17
17
|
HStack(spacing: 12) {
|
|
@@ -28,8 +28,9 @@ struct SourceMapExplorerView: View {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
.navigationTitle("Source Code Explorer")
|
|
31
|
-
#if !os(macOS)
|
|
31
|
+
#if !os(macOS) && !os(tvOS)
|
|
32
32
|
.navigationBarTitleDisplayMode(.inline)
|
|
33
|
+
.navigationBarHidden(false)
|
|
33
34
|
#endif
|
|
34
35
|
.searchable(text: $viewModel.searchText, placement: .automatic, prompt: "Search files")
|
|
35
36
|
.task {
|
|
@@ -64,7 +65,9 @@ struct SourceMapExplorerView: View {
|
|
|
64
65
|
Button("Retry") {
|
|
65
66
|
Task { await viewModel.loadSourceMap() }
|
|
66
67
|
}
|
|
68
|
+
#if !os(tvOS)
|
|
67
69
|
.buttonStyle(.borderedProminent)
|
|
70
|
+
#endif
|
|
68
71
|
}
|
|
69
72
|
.padding()
|
|
70
73
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
@@ -85,43 +88,60 @@ struct FolderListView: View {
|
|
|
85
88
|
.foregroundColor(.secondary)
|
|
86
89
|
} else {
|
|
87
90
|
ForEach(nodes) { node in
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
title: node.name,
|
|
91
|
-
nodes: node.children,
|
|
92
|
-
sourceMap: sourceMap,
|
|
93
|
-
stats: nil,
|
|
94
|
-
isSearching: false
|
|
95
|
-
)) {
|
|
96
|
-
FileRow(node: node, showPath: isSearching)
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
NavigationLink(destination: CodeFileView(node: node, sourceMap: sourceMap)) {
|
|
100
|
-
FileRow(node: node, showPath: isSearching)
|
|
101
|
-
}
|
|
91
|
+
NavigationLink(destination: destinationView(for: node)) {
|
|
92
|
+
FileRow(node: node, showPath: isSearching)
|
|
102
93
|
}
|
|
103
94
|
}
|
|
104
95
|
}
|
|
105
96
|
}
|
|
106
|
-
#if !os(macOS)
|
|
97
|
+
#if !os(macOS) && !os(tvOS)
|
|
107
98
|
.listStyle(.insetGrouped)
|
|
99
|
+
#elseif os(tvOS)
|
|
100
|
+
.listStyle(.plain)
|
|
108
101
|
#endif
|
|
109
102
|
.navigationTitle(isSearching ? "Search Results" : title)
|
|
110
|
-
#if !os(macOS)
|
|
103
|
+
#if !os(macOS) && !os(tvOS)
|
|
111
104
|
.navigationBarTitleDisplayMode(.inline)
|
|
105
|
+
#endif
|
|
112
106
|
.toolbar {
|
|
113
107
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
114
108
|
if let stats = stats {
|
|
109
|
+
#if os(tvOS)
|
|
110
|
+
if #available(tvOS 17.0, *) {
|
|
111
|
+
Menu {
|
|
112
|
+
Label("\(stats.files) files", systemImage: "doc.on.doc")
|
|
113
|
+
Label(stats.totalSize, systemImage: "internaldrive")
|
|
114
|
+
} label: {
|
|
115
|
+
Image(systemName: "info.circle")
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Menu not available on tvOS < 17.0, toolbar item is hidden
|
|
119
|
+
#else
|
|
115
120
|
Menu {
|
|
116
121
|
Label("\(stats.files) files", systemImage: "doc.on.doc")
|
|
117
122
|
Label(stats.totalSize, systemImage: "internaldrive")
|
|
118
123
|
} label: {
|
|
119
124
|
Image(systemName: "info.circle")
|
|
120
125
|
}
|
|
126
|
+
#endif
|
|
121
127
|
}
|
|
122
128
|
}
|
|
123
129
|
}
|
|
124
|
-
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@ViewBuilder
|
|
133
|
+
private func destinationView(for node: FileTreeNode) -> some View {
|
|
134
|
+
if node.isDirectory {
|
|
135
|
+
FolderListView(
|
|
136
|
+
title: node.name,
|
|
137
|
+
nodes: node.children,
|
|
138
|
+
sourceMap: sourceMap,
|
|
139
|
+
stats: nil,
|
|
140
|
+
isSearching: false
|
|
141
|
+
)
|
|
142
|
+
} else {
|
|
143
|
+
CodeFileView(node: node, sourceMap: sourceMap)
|
|
144
|
+
}
|
|
125
145
|
}
|
|
126
146
|
}
|
|
127
147
|
|
|
@@ -183,8 +203,10 @@ struct CodeFileView: View {
|
|
|
183
203
|
let sourceMap: SourceMap?
|
|
184
204
|
@Environment(\.colorScheme) private var colorScheme
|
|
185
205
|
@State private var highlightedLines: [AttributedString]?
|
|
206
|
+
@State private var isEditing = false
|
|
207
|
+
@State private var displayContent: String = ""
|
|
186
208
|
|
|
187
|
-
private var
|
|
209
|
+
private var originalContent: String {
|
|
188
210
|
guard let contentIndex = node.contentIndex,
|
|
189
211
|
let sourceMap,
|
|
190
212
|
let sourcesContent = sourceMap.sourcesContent,
|
|
@@ -196,7 +218,7 @@ struct CodeFileView: View {
|
|
|
196
218
|
}
|
|
197
219
|
|
|
198
220
|
private var lines: [String] {
|
|
199
|
-
|
|
221
|
+
displayContent.components(separatedBy: "\n")
|
|
200
222
|
}
|
|
201
223
|
|
|
202
224
|
private var theme: SyntaxHighlighter.Theme {
|
|
@@ -209,35 +231,83 @@ struct CodeFileView: View {
|
|
|
209
231
|
}
|
|
210
232
|
|
|
211
233
|
var body: some View {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
CodeColumn(lines: lines, highlightedLines: highlightedLines, theme: theme)
|
|
218
|
-
}
|
|
219
|
-
.frame(minWidth: geometry.size.width, alignment: .leading)
|
|
220
|
-
}
|
|
234
|
+
Group {
|
|
235
|
+
if isEditing {
|
|
236
|
+
editingView()
|
|
237
|
+
} else {
|
|
238
|
+
readOnlyView()
|
|
221
239
|
}
|
|
222
240
|
}
|
|
223
241
|
.background(theme.background)
|
|
224
242
|
.navigationTitle(node.name)
|
|
225
|
-
#if !os(macOS)
|
|
243
|
+
#if !os(macOS) && !os(tvOS)
|
|
226
244
|
.navigationBarTitleDisplayMode(.inline)
|
|
245
|
+
.toolbar {
|
|
246
|
+
ToolbarItem(placement: .navigationBarTrailing) {
|
|
247
|
+
Button(isEditing ? "Done" : "Edit") {
|
|
248
|
+
if isEditing {
|
|
249
|
+
isEditing = false
|
|
250
|
+
Task {
|
|
251
|
+
highlightedLines = await SyntaxHighlighter.highlightLines(lines, theme: theme)
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
isEditing = true
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
227
259
|
#endif
|
|
260
|
+
.onAppear {
|
|
261
|
+
if displayContent.isEmpty {
|
|
262
|
+
displayContent = originalContent
|
|
263
|
+
}
|
|
264
|
+
}
|
|
228
265
|
.task(id: colorScheme) {
|
|
229
266
|
highlightedLines = await SyntaxHighlighter.highlightLines(lines, theme: theme)
|
|
230
267
|
}
|
|
231
268
|
}
|
|
269
|
+
|
|
270
|
+
private func readOnlyView() -> some View {
|
|
271
|
+
ScrollView(.vertical) {
|
|
272
|
+
ScrollView(.horizontal, showsIndicators: false) {
|
|
273
|
+
HStack(alignment: .top, spacing: 0) {
|
|
274
|
+
LineNumbersColumn(lines: lines, theme: theme, lineNumberWidth: lineNumberWidth)
|
|
275
|
+
CodeColumn(lines: lines, highlightedLines: highlightedLines, theme: theme)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private func editingView() -> some View {
|
|
282
|
+
TextEditor(text: $displayContent)
|
|
283
|
+
.font(.system(size: 13, weight: .regular, design: .monospaced))
|
|
284
|
+
#if os(iOS) || os(tvOS)
|
|
285
|
+
.textInputAutocapitalization(.never)
|
|
286
|
+
#endif
|
|
287
|
+
.autocorrectionDisabled()
|
|
288
|
+
.modifier(ScrollContentBackgroundModifier())
|
|
289
|
+
.background(theme.background)
|
|
290
|
+
.foregroundColor(theme.plain)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private struct ScrollContentBackgroundModifier: ViewModifier {
|
|
295
|
+
func body(content: Content) -> some View {
|
|
296
|
+
if #available(iOS 16.0, tvOS 16.0, *) {
|
|
297
|
+
content.scrollContentBackground(.hidden)
|
|
298
|
+
} else {
|
|
299
|
+
content
|
|
300
|
+
}
|
|
301
|
+
}
|
|
232
302
|
}
|
|
233
303
|
|
|
234
304
|
struct LineNumbersColumn: View {
|
|
235
305
|
let lines: [String]
|
|
236
306
|
let theme: SyntaxHighlighter.Theme
|
|
237
307
|
let lineNumberWidth: CGFloat
|
|
238
|
-
|
|
308
|
+
|
|
239
309
|
var body: some View {
|
|
240
|
-
|
|
310
|
+
VStack(alignment: .trailing, spacing: 0) {
|
|
241
311
|
ForEach(0..<lines.count, id: \.self) { index in
|
|
242
312
|
Text("\(index + 1)")
|
|
243
313
|
.font(.system(size: 13, weight: .regular, design: .monospaced))
|
|
@@ -255,9 +325,9 @@ struct CodeColumn: View {
|
|
|
255
325
|
let lines: [String]
|
|
256
326
|
let highlightedLines: [AttributedString]?
|
|
257
327
|
let theme: SyntaxHighlighter.Theme
|
|
258
|
-
|
|
328
|
+
|
|
259
329
|
var body: some View {
|
|
260
|
-
|
|
330
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
261
331
|
ForEach(0..<lines.count, id: \.self) { index in
|
|
262
332
|
if let highlightedLines, index < highlightedLines.count {
|
|
263
333
|
Text(highlightedLines[index])
|
|
@@ -271,13 +341,8 @@ struct CodeColumn: View {
|
|
|
271
341
|
}
|
|
272
342
|
}
|
|
273
343
|
}
|
|
344
|
+
.fixedSize(horizontal: true, vertical: false)
|
|
274
345
|
.padding(.vertical, 12)
|
|
275
346
|
.padding(.trailing, 16)
|
|
276
347
|
}
|
|
277
348
|
}
|
|
278
|
-
|
|
279
|
-
#Preview {
|
|
280
|
-
NavigationView {
|
|
281
|
-
SourceMapExplorerView()
|
|
282
|
-
}
|
|
283
|
-
}
|
|
@@ -184,7 +184,7 @@ class SourceMapService {
|
|
|
184
184
|
private func extractInlineSourceMap(from bundleContent: String) throws -> SourceMap {
|
|
185
185
|
let patterns = [
|
|
186
186
|
"//# sourceMappingURL=data:application/json;charset=utf-8;base64,",
|
|
187
|
-
"//# sourceMappingURL=data:application/json;base64,"
|
|
187
|
+
"//# sourceMappingURL=data:application/json;base64,"
|
|
188
188
|
]
|
|
189
189
|
|
|
190
190
|
for pattern in patterns {
|
|
@@ -234,7 +234,14 @@ class SourceMapService {
|
|
|
234
234
|
|
|
235
235
|
let nodes = root.children.values.map { convertToNode($0) }
|
|
236
236
|
let sorted = sortNodes(nodes)
|
|
237
|
-
|
|
237
|
+
let collapsed = collapseSingleChildFolders(sorted)
|
|
238
|
+
|
|
239
|
+
let directories = collapsed.filter { $0.isDirectory }
|
|
240
|
+
if directories.count == 1, let mainDir = directories.first {
|
|
241
|
+
return mainDir.children
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return collapsed
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
private func convertToNode(_ builder: Node) -> FileTreeNode {
|
|
@@ -255,7 +262,7 @@ class SourceMapService {
|
|
|
255
262
|
while current.isDirectory && current.children.count == 1 && current.children[0].isDirectory {
|
|
256
263
|
let child = current.children[0]
|
|
257
264
|
current = FileTreeNode(
|
|
258
|
-
name:
|
|
265
|
+
name: child.name,
|
|
259
266
|
path: child.path,
|
|
260
267
|
isDirectory: true,
|
|
261
268
|
children: child.children,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-dev-menu",
|
|
3
|
-
"version": "55.0.
|
|
3
|
+
"version": "55.0.3",
|
|
4
4
|
"description": "Expo/React Native module with the developer menu.",
|
|
5
5
|
"main": "build/DevMenu.js",
|
|
6
6
|
"types": "build/DevMenu.d.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"homepage": "https://docs.expo.dev",
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"expo-dev-menu-interface": "55.0.
|
|
36
|
+
"expo-dev-menu-interface": "55.0.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@babel/preset-typescript": "^7.7.4",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"expo": "*"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "220594d473a3100248087151004ae4acb7282d5f"
|
|
51
51
|
}
|