fix: Reopen last component from previous session (#262)
This commit is contained in:
Родитель
0e11850e2a
Коммит
6f64e208e0
|
@ -61,6 +61,11 @@ android {
|
|||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
allWarningsAsErrors = true
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId project.ext.react.applicationId
|
||||
minSdkVersion 21
|
||||
|
|
|
@ -14,7 +14,6 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.bridge.ReactContext
|
||||
import com.facebook.react.modules.systeminfo.ReactNativeVersion
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.microsoft.reacttestapp.component.ComponentActivity
|
||||
|
@ -37,8 +36,7 @@ class MainActivity : ReactActivity() {
|
|||
@Inject
|
||||
lateinit var bundleNameProvider: ReactBundleNameProvider
|
||||
|
||||
private val testAppReactNativeHost: TestAppReactNativeHost
|
||||
get() = reactNativeHost as TestAppReactNativeHost
|
||||
private var didInitialNavigation = false
|
||||
|
||||
private val newComponentViewModel = { component: Component ->
|
||||
ComponentViewModel(
|
||||
|
@ -49,8 +47,12 @@ class MainActivity : ReactActivity() {
|
|||
)
|
||||
}
|
||||
|
||||
private val startComponent: (ComponentViewModel) -> Unit = { component: ComponentViewModel ->
|
||||
didInitialNavigation = true;
|
||||
private val session by lazy {
|
||||
Session(applicationContext)
|
||||
}
|
||||
|
||||
private val startComponent: (ComponentViewModel) -> Unit = { component ->
|
||||
didInitialNavigation = true
|
||||
when (component.presentationStyle) {
|
||||
"modal" -> {
|
||||
ComponentBottomSheetDialogFragment
|
||||
|
@ -63,21 +65,24 @@ class MainActivity : ReactActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private var didInitialNavigation = false;
|
||||
private val testAppReactNativeHost: TestAppReactNativeHost
|
||||
get() = reactNativeHost as TestAppReactNativeHost
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
AndroidInjection.inject(this)
|
||||
didInitialNavigation = savedInstanceState?.getBoolean("didInitialNavigation", false) == true
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
val manifest = manifestProvider.manifest
|
||||
didInitialNavigation = savedInstanceState?.getBoolean("didInitialNavigation", false) == true
|
||||
|
||||
val (manifest, checksum) = manifestProvider.fromResources()
|
||||
?: throw IllegalStateException("app.json is not provided or TestApp is misconfigured")
|
||||
|
||||
if (manifest.components.count() == 1) {
|
||||
val component = newComponentViewModel(manifest.components[0])
|
||||
testAppReactNativeHost.addReactInstanceEventListener { _: ReactContext ->
|
||||
val index = if (manifest.components.count() == 1) 0 else session.lastOpenedComponent(checksum)
|
||||
index?.let {
|
||||
val component = newComponentViewModel(manifest.components[it])
|
||||
testAppReactNativeHost.addReactInstanceEventListener {
|
||||
if (!didInitialNavigation) {
|
||||
startComponent(component)
|
||||
}
|
||||
|
@ -85,14 +90,44 @@ class MainActivity : ReactActivity() {
|
|||
}
|
||||
|
||||
setupToolbar(manifest.displayName)
|
||||
setupRecyclerView(manifest.components)
|
||||
setupRecyclerView(manifest.components, checksum)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean("didInitialNavigation", didInitialNavigation);
|
||||
outState.putBoolean("didInitialNavigation", didInitialNavigation)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun reload(bundleSource: BundleSource) {
|
||||
testAppReactNativeHost.reload(this, bundleSource)
|
||||
}
|
||||
|
||||
private fun setupRecyclerView(manifestComponents: List<Component>, manifestChecksum: String) {
|
||||
val components = manifestComponents.map(newComponentViewModel)
|
||||
findViewById<RecyclerView>(R.id.recyclerview).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = ComponentListAdapter(
|
||||
LayoutInflater.from(context),
|
||||
components
|
||||
) { component, index ->
|
||||
startComponent(component)
|
||||
session.storeComponent(index, manifestChecksum)
|
||||
}
|
||||
|
||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||
}
|
||||
|
||||
findViewById<TextView>(R.id.runtime_info).apply {
|
||||
text = resources.getString(
|
||||
R.string.runtime_info,
|
||||
ReactNativeVersion.VERSION["major"] as Int,
|
||||
ReactNativeVersion.VERSION["minor"] as Int,
|
||||
ReactNativeVersion.VERSION["patch"] as Int,
|
||||
reactInstanceManager.jsExecutorName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupToolbar(displayName: String) {
|
||||
val toolbar = findViewById<MaterialToolbar>(R.id.top_app_bar)
|
||||
|
||||
|
@ -107,6 +142,12 @@ class MainActivity : ReactActivity() {
|
|||
reload(BundleSource.Server)
|
||||
true
|
||||
}
|
||||
R.id.remember_last_component -> {
|
||||
val enable = !menuItem.isChecked
|
||||
menuItem.isChecked = enable
|
||||
session.shouldRememberLastComponent = enable
|
||||
true
|
||||
}
|
||||
R.id.show_dev_options -> {
|
||||
reactInstanceManager.devSupportManager.showDevOptionsDialog()
|
||||
true
|
||||
|
@ -121,36 +162,11 @@ class MainActivity : ReactActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun reload(bundleSource: BundleSource) {
|
||||
testAppReactNativeHost.reload(this, bundleSource)
|
||||
}
|
||||
|
||||
private fun updateMenuItemState(toolbar: MaterialToolbar, bundleSource: BundleSource) {
|
||||
toolbar.menu.apply {
|
||||
findItem(R.id.load_embedded_js_bundle).isEnabled = bundleNameProvider.bundleName != null
|
||||
findItem(R.id.remember_last_component).isChecked = session.shouldRememberLastComponent
|
||||
findItem(R.id.show_dev_options).isEnabled = bundleSource == BundleSource.Server
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView(manifestComponents: List<Component>) {
|
||||
val components = manifestComponents.map(newComponentViewModel)
|
||||
findViewById<RecyclerView>(R.id.recyclerview).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = ComponentListAdapter(
|
||||
LayoutInflater.from(context), components, startComponent
|
||||
)
|
||||
|
||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||
}
|
||||
|
||||
findViewById<TextView>(R.id.runtime_info).apply {
|
||||
text = resources.getString(
|
||||
R.string.runtime_info,
|
||||
ReactNativeVersion.VERSION["major"] as Int,
|
||||
ReactNativeVersion.VERSION["minor"] as Int,
|
||||
ReactNativeVersion.VERSION["patch"] as Int,
|
||||
reactInstanceManager.jsExecutorName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
package com.microsoft.reacttestapp
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
|
||||
private const val MANIFEST_CHECKSUM = "ManifestChecksum"
|
||||
private const val REMEMBER_LAST_COMPONENT_ENABLED = "RememberLastComponent/Enabled"
|
||||
private const val REMEMBER_LAST_COMPONENT_INDEX = "RememberLastComponent/Index"
|
||||
|
||||
class Session(context: Context) {
|
||||
|
||||
private val sharedPreferences: SharedPreferences =
|
||||
context.getSharedPreferences(BuildConfig.APPLICATION_ID, MODE_PRIVATE)
|
||||
|
||||
private var lastComponentIndex: Int
|
||||
get() = sharedPreferences.getInt(REMEMBER_LAST_COMPONENT_INDEX, -1)
|
||||
set(index) = sharedPreferences.edit { putInt(REMEMBER_LAST_COMPONENT_INDEX, index) }
|
||||
|
||||
private var manifestChecksum: String?
|
||||
get() = sharedPreferences.getString(MANIFEST_CHECKSUM, null)
|
||||
set(checksum) = sharedPreferences.edit { putString(MANIFEST_CHECKSUM, checksum) }
|
||||
|
||||
var shouldRememberLastComponent: Boolean
|
||||
get() = sharedPreferences.getBoolean(REMEMBER_LAST_COMPONENT_ENABLED, false)
|
||||
set(enabled) = sharedPreferences.edit { putBoolean(REMEMBER_LAST_COMPONENT_ENABLED, enabled) }
|
||||
|
||||
fun lastOpenedComponent(checksum: String): Int? {
|
||||
if (!shouldRememberLastComponent || checksum != manifestChecksum) {
|
||||
return null
|
||||
}
|
||||
|
||||
val index = lastComponentIndex
|
||||
return if (index < 0) null else index
|
||||
}
|
||||
|
||||
fun storeComponent(index: Int, checksum: String) {
|
||||
lastComponentIndex = index
|
||||
manifestChecksum = checksum
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ import com.microsoft.reacttestapp.R
|
|||
class ComponentListAdapter(
|
||||
private val layoutInflater: LayoutInflater,
|
||||
private val components: List<ComponentViewModel>,
|
||||
private val listener: (ComponentViewModel) -> Unit
|
||||
private val listener: (ComponentViewModel, Int) -> Unit
|
||||
) : RecyclerView.Adapter<ComponentListAdapter.ComponentViewHolder>() {
|
||||
|
||||
override fun getItemCount() = components.size
|
||||
|
@ -28,7 +28,7 @@ class ComponentListAdapter(
|
|||
|
||||
inner class ComponentViewHolder(private val view: TextView) : RecyclerView.ViewHolder(view) {
|
||||
init {
|
||||
view.setOnClickListener { listener(components[adapterPosition]) }
|
||||
view.setOnClickListener { listener(components[adapterPosition], adapterPosition) }
|
||||
}
|
||||
|
||||
fun bindTo(component: ComponentViewModel) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.microsoft.reacttestapp.manifest
|
|||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import java.security.MessageDigest
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
@ -10,16 +11,27 @@ class ManifestProvider @Inject constructor(
|
|||
private val context: Context,
|
||||
private val adapter: JsonAdapter<Manifest>
|
||||
) {
|
||||
val manifest: Manifest? by lazy {
|
||||
fun fromResources(): Pair<Manifest, String>? {
|
||||
val appIdentifier = context.resources
|
||||
.getIdentifier("raw/app", null, context.packageName)
|
||||
|
||||
if (appIdentifier != 0) {
|
||||
val manifest = context.resources
|
||||
return if (appIdentifier == 0) {
|
||||
null
|
||||
} else {
|
||||
val json = context.resources
|
||||
.openRawResource(appIdentifier)
|
||||
.bufferedReader()
|
||||
.use { it.readText() }
|
||||
adapter.fromJson(manifest)
|
||||
} else null
|
||||
val manifest = adapter.fromJson(json)
|
||||
return if (manifest == null) null else Pair(manifest, json.checksum("SHA-256"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.toHex(): String {
|
||||
return joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
fun String.checksum(algorithm: String): String {
|
||||
return MessageDigest.getInstance(algorithm).digest(toByteArray()).toHex()
|
||||
}
|
||||
|
|
|
@ -11,6 +11,12 @@
|
|||
android:contentDescription="@string/content_description_load_from_dev_server"
|
||||
android:title="@string/load_from_dev_server"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/remember_last_component"
|
||||
android:checkable="true"
|
||||
android:contentDescription="@string/content_description_remember_last_component"
|
||||
android:title="@string/remember_last_component"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/show_dev_options"
|
||||
android:contentDescription="@string/content_description_show_dev_options"
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
<string name="load_embedded_js_bundle">Load embedded JS bundle</string>
|
||||
<string name="content_description_load_from_dev_server">Load from dev server</string>
|
||||
<string name="load_from_dev_server">Load from dev server</string>
|
||||
<string name="content_description_remember_last_component">Remember last opened component</string>
|
||||
<string name="remember_last_component">Remember last opened component</string>
|
||||
<string name="content_description_show_dev_options">Show dev options</string>
|
||||
<string name="show_dev_options">Show dev options</string>
|
||||
<string name="runtime_info">React Native v%1$d.%2$d.%3$d\n(%4$s)</string>
|
||||
|
|
|
@ -11,11 +11,13 @@ import XCTest
|
|||
class ManifestTests: XCTestCase {
|
||||
|
||||
func testReadFromFile() {
|
||||
guard let manifest = Manifest.fromFile() else {
|
||||
guard let (manifest, checksum) = Manifest.fromFile() else {
|
||||
XCTFail("Failed to read 'app.json'")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(checksum.count, 64)
|
||||
|
||||
XCTAssertEqual(manifest.name, "Example")
|
||||
XCTAssertEqual(manifest.displayName, "Example")
|
||||
XCTAssertEqual(manifest.components.count, 2)
|
||||
|
@ -71,11 +73,13 @@ class ManifestTests: XCTestCase {
|
|||
"""
|
||||
|
||||
guard let data = json.data(using: .utf8),
|
||||
let manifest = Manifest.from(data: data) else {
|
||||
let (manifest, checksum) = Manifest.from(data: data) else {
|
||||
XCTFail("Failed to read manifest")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(checksum.count, 64)
|
||||
|
||||
XCTAssertEqual(manifest.name, expected.name)
|
||||
XCTAssertEqual(manifest.displayName, expected.displayName)
|
||||
XCTAssertEqual(manifest.components.count, expected.components.count)
|
||||
|
@ -136,7 +140,7 @@ class ManifestTests: XCTestCase {
|
|||
"""
|
||||
|
||||
guard let data = json.data(using: .utf8),
|
||||
let manifest = Manifest.from(data: data),
|
||||
let (manifest, _) = Manifest.from(data: data),
|
||||
let component = manifest.components.first,
|
||||
let initialProperties = component.initialProperties else {
|
||||
XCTFail("Failed to read manifest")
|
||||
|
|
|
@ -26,58 +26,58 @@ namespace ReactTestAppTests
|
|||
public:
|
||||
TEST_METHOD(ParseManifestWithOneComponent)
|
||||
{
|
||||
std::optional<ReactTestApp::Manifest> manifest =
|
||||
ReactTestApp::GetManifest("manifestTestFiles/simpleManifest.json");
|
||||
if (!manifest.has_value()) {
|
||||
auto result = ReactTestApp::GetManifest("manifestTestFiles/simpleManifest.json");
|
||||
if (!result.has_value()) {
|
||||
Assert::Fail(L"Couldn't read manifest file");
|
||||
}
|
||||
|
||||
auto &manifestContents = manifest.value();
|
||||
Assert::AreEqual(manifestContents.name, {"Example"});
|
||||
Assert::AreEqual(manifestContents.displayName, {"Example"});
|
||||
Assert::AreEqual(manifestContents.components[0].appKey, {"Example"});
|
||||
Assert::AreEqual(manifestContents.components[0].displayName.value(), {"App"});
|
||||
Assert::IsFalse(manifestContents.components[0].initialProperties.has_value());
|
||||
auto&& [manifest, checksum] = result.value();
|
||||
|
||||
Assert::AreEqual(manifest.name, {"Example"});
|
||||
Assert::AreEqual(manifest.displayName, {"Example"});
|
||||
Assert::AreEqual(manifest.components[0].appKey, {"Example"});
|
||||
Assert::AreEqual(manifest.components[0].displayName.value(), {"App"});
|
||||
Assert::IsFalse(manifest.components[0].initialProperties.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseManifestWithMultipleComponents)
|
||||
{
|
||||
std::optional<ReactTestApp::Manifest> manifest =
|
||||
ReactTestApp::GetManifest("manifestTestFiles/withMultipleComponents.json");
|
||||
if (!manifest.has_value()) {
|
||||
auto result = ReactTestApp::GetManifest("manifestTestFiles/withMultipleComponents.json");
|
||||
if (!result.has_value()) {
|
||||
Assert::Fail(L"Couldn't read manifest file");
|
||||
}
|
||||
|
||||
auto &manifestContents = manifest.value();
|
||||
Assert::AreEqual(manifestContents.name, {"Example"});
|
||||
Assert::AreEqual(manifestContents.displayName, {"Example"});
|
||||
Assert::AreEqual(manifestContents.components.size(), {2});
|
||||
auto&& [manifest, checksum] = result.value();
|
||||
|
||||
Assert::AreEqual(manifestContents.components[0].appKey, {"0"});
|
||||
Assert::IsFalse(manifestContents.components[0].displayName.has_value());
|
||||
Assert::IsTrue(manifestContents.components[0].initialProperties.has_value());
|
||||
Assert::AreEqual(manifest.name, {"Example"});
|
||||
Assert::AreEqual(manifest.displayName, {"Example"});
|
||||
Assert::AreEqual(manifest.components.size(), {2});
|
||||
|
||||
Assert::AreEqual(manifest.components[0].appKey, {"0"});
|
||||
Assert::IsFalse(manifest.components[0].displayName.has_value());
|
||||
Assert::IsTrue(manifest.components[0].initialProperties.has_value());
|
||||
Assert::AreEqual(std::any_cast<std::string>(
|
||||
manifestContents.components[0].initialProperties.value()["key"]),
|
||||
manifest.components[0].initialProperties.value()["key"]),
|
||||
{"value"});
|
||||
|
||||
Assert::AreEqual(manifestContents.components[1].appKey, {"1"});
|
||||
Assert::AreEqual(manifestContents.components[1].displayName.value(), {"1"});
|
||||
Assert::IsFalse(manifestContents.components[1].initialProperties.has_value());
|
||||
Assert::AreEqual(manifest.components[1].appKey, {"1"});
|
||||
Assert::AreEqual(manifest.components[1].displayName.value(), {"1"});
|
||||
Assert::IsFalse(manifest.components[1].initialProperties.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseManifestWithComplexInitialProperties)
|
||||
{
|
||||
std::optional<ReactTestApp::Manifest> manifest =
|
||||
ReactTestApp::GetManifest("manifestTestFiles/withComplexInitialProperties.json");
|
||||
if (!manifest.has_value()) {
|
||||
auto result = ReactTestApp::GetManifest("manifestTestFiles/withComplexInitialProperties.json");
|
||||
if (!result.has_value()) {
|
||||
Assert::Fail(L"Couldn't read manifest file");
|
||||
}
|
||||
|
||||
auto &manifestContents = manifest.value();
|
||||
auto &component = manifestContents.components[0];
|
||||
Assert::AreEqual(manifestContents.name, {"Name"});
|
||||
Assert::AreEqual(manifestContents.displayName, {"Display Name"});
|
||||
auto&& [manifest, checksum] = result.value();
|
||||
|
||||
Assert::AreEqual(manifest.name, {"Name"});
|
||||
Assert::AreEqual(manifest.displayName, {"Display Name"});
|
||||
|
||||
auto &component = manifest.components[0];
|
||||
Assert::AreEqual(component.appKey, {"AppKey"});
|
||||
Assert::IsFalse(component.displayName.has_value());
|
||||
Assert::IsTrue(component.initialProperties.has_value());
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
196C724123319A85006556ED /* QRCodeReaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196C724023319A85006556ED /* QRCodeReaderDelegate.swift */; };
|
||||
196C7215232F1788006556ED /* ReactInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196C7214232F1788006556ED /* ReactInstance.swift */; };
|
||||
19ECD0D8232ED425003D8557 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19ECD0D7232ED425003D8557 /* SceneDelegate.swift */; };
|
||||
19A624A4258C95F000032776 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A624A3258C95F000032776 /* Session.swift */; };
|
||||
1914199A234B2DD800D856AE /* RCTDevSupport+UIScene.m in Sources */ = {isa = PBXBuildFile; fileRef = 19141999234B2DD800D856AE /* RCTDevSupport+UIScene.m */; };
|
||||
196C22622490CB7600449D3C /* React+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 196C22602490CB7600449D3C /* React+Compatibility.m */; };
|
||||
1988284524105BEC005057FF /* UIViewController+ReactTestApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 1988284424105BEC005057FF /* UIViewController+ReactTestApp.m */; };
|
||||
|
@ -40,24 +41,25 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
19141999234B2DD800D856AE /* RCTDevSupport+UIScene.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "RCTDevSupport+UIScene.m"; sourceTree = "<group>"; };
|
||||
192DD200240FCAF5004E9CEB /* Manifest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Manifest.swift; sourceTree = "<group>"; };
|
||||
192F052624AD3CC500A48456 /* ReactTestApp.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.release.xcconfig; sourceTree = "<group>"; };
|
||||
192F052724AD3CC500A48456 /* ReactTestApp.common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.common.xcconfig; sourceTree = "<group>"; };
|
||||
192F052824AD3CC500A48456 /* ReactTestApp.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.debug.xcconfig; sourceTree = "<group>"; };
|
||||
196C22602490CB7600449D3C /* React+Compatibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "React+Compatibility.m"; sourceTree = "<group>"; };
|
||||
196C22612490CB7600449D3C /* React+Compatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "React+Compatibility.h"; sourceTree = "<group>"; };
|
||||
196C7207232EF5DC006556ED /* ReactTestApp-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactTestApp-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
196C7214232F1788006556ED /* ReactInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactInstance.swift; sourceTree = "<group>"; };
|
||||
196C7216232F6CD9006556ED /* ReactTestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ReactTestApp.entitlements; sourceTree = "<group>"; };
|
||||
196C724023319A85006556ED /* QRCodeReaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeReaderDelegate.swift; sourceTree = "<group>"; };
|
||||
1988282224105BCC005057FF /* UIViewController+ReactTestApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+ReactTestApp.h"; sourceTree = "<group>"; };
|
||||
1988284424105BEC005057FF /* UIViewController+ReactTestApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+ReactTestApp.m"; sourceTree = "<group>"; };
|
||||
19ECD0D2232ED425003D8557 /* ReactTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
19ECD0D5232ED425003D8557 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
19ECD0D7232ED425003D8557 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
19ECD0D9232ED425003D8557 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
192DD200240FCAF5004E9CEB /* Manifest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Manifest.swift; sourceTree = "<group>"; };
|
||||
196C724023319A85006556ED /* QRCodeReaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeReaderDelegate.swift; sourceTree = "<group>"; };
|
||||
196C7214232F1788006556ED /* ReactInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactInstance.swift; sourceTree = "<group>"; };
|
||||
19ECD0D7232ED425003D8557 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
19A624A3258C95F000032776 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = "<group>"; };
|
||||
196C7207232EF5DC006556ED /* ReactTestApp-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactTestApp-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
19141999234B2DD800D856AE /* RCTDevSupport+UIScene.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "RCTDevSupport+UIScene.m"; sourceTree = "<group>"; };
|
||||
196C22612490CB7600449D3C /* React+Compatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "React+Compatibility.h"; sourceTree = "<group>"; };
|
||||
196C22602490CB7600449D3C /* React+Compatibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "React+Compatibility.m"; sourceTree = "<group>"; };
|
||||
1988282224105BCC005057FF /* UIViewController+ReactTestApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+ReactTestApp.h"; sourceTree = "<group>"; };
|
||||
1988284424105BEC005057FF /* UIViewController+ReactTestApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+ReactTestApp.m"; sourceTree = "<group>"; };
|
||||
19ECD0DB232ED427003D8557 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
196C7216232F6CD9006556ED /* ReactTestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ReactTestApp.entitlements; sourceTree = "<group>"; };
|
||||
192F052724AD3CC500A48456 /* ReactTestApp.common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.common.xcconfig; sourceTree = "<group>"; };
|
||||
192F052824AD3CC500A48456 /* ReactTestApp.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.debug.xcconfig; sourceTree = "<group>"; };
|
||||
192F052624AD3CC500A48456 /* ReactTestApp.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.release.xcconfig; sourceTree = "<group>"; };
|
||||
19ECD0E1232ED427003D8557 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
19ECD0E3232ED427003D8557 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
19ECD0E8232ED427003D8557 /* ReactTestAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactTestAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -122,6 +124,7 @@
|
|||
192DD200240FCAF5004E9CEB /* Manifest.swift */,
|
||||
196C724023319A85006556ED /* QRCodeReaderDelegate.swift */,
|
||||
196C7214232F1788006556ED /* ReactInstance.swift */,
|
||||
19A624A3258C95F000032776 /* Session.swift */,
|
||||
196C7207232EF5DC006556ED /* ReactTestApp-Bridging-Header.h */,
|
||||
19141999234B2DD800D856AE /* RCTDevSupport+UIScene.m */,
|
||||
196C22612490CB7600449D3C /* React+Compatibility.h */,
|
||||
|
@ -319,6 +322,7 @@
|
|||
196C22622490CB7600449D3C /* React+Compatibility.m in Sources */,
|
||||
196C7215232F1788006556ED /* ReactInstance.swift in Sources */,
|
||||
19ECD0D8232ED425003D8557 /* SceneDelegate.swift in Sources */,
|
||||
19A624A4258C95F000032776 /* Session.swift in Sources */,
|
||||
1988284524105BEC005057FF /* UIViewController+ReactTestApp.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -8,7 +8,23 @@
|
|||
import QRCodeReader
|
||||
import UIKit
|
||||
|
||||
typealias NavigationLink = (String, () -> Void)
|
||||
private struct NavigationLink {
|
||||
let title: String
|
||||
let action: (() -> Void)?
|
||||
let accessoryView: UIView?
|
||||
|
||||
init(title: String, action: @escaping () -> Void) {
|
||||
self.title = title
|
||||
self.action = action
|
||||
accessoryView = nil
|
||||
}
|
||||
|
||||
init(title: String, accessoryView: UIView) {
|
||||
self.title = title
|
||||
action = nil
|
||||
self.accessoryView = accessoryView
|
||||
}
|
||||
}
|
||||
|
||||
private struct SectionData {
|
||||
var items: [NavigationLink]
|
||||
|
@ -16,6 +32,11 @@ private struct SectionData {
|
|||
}
|
||||
|
||||
final class ContentViewController: UITableViewController {
|
||||
private enum Section {
|
||||
static let components = 0
|
||||
static let settings = 1
|
||||
}
|
||||
|
||||
private let reactInstance: ReactInstance
|
||||
private var sections: [SectionData]
|
||||
|
||||
|
@ -27,18 +48,6 @@ final class ContentViewController: UITableViewController {
|
|||
sections = []
|
||||
|
||||
super.init(style: .grouped)
|
||||
|
||||
title = "ReactTestApp"
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
let keyboardShortcut = " (⌃⌘Z)"
|
||||
#else
|
||||
let keyboardShortcut = ""
|
||||
#endif
|
||||
sections.append(SectionData(
|
||||
items: [],
|
||||
footer: "\(runtimeInfo())\n\nShake your device\(keyboardShortcut) to open the React Native debug menu."
|
||||
))
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
|
@ -51,26 +60,55 @@ final class ContentViewController: UITableViewController {
|
|||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
guard let (manifest, checksum) = Manifest.fromFile() else {
|
||||
return
|
||||
}
|
||||
|
||||
let components = manifest.components
|
||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||
self?.reactInstance.initReact { components in
|
||||
let items: [NavigationLink] = components.map { component in
|
||||
(component.displayName ?? component.appKey, { self?.navigate(to: component) })
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
self?.reactInstance.initReact {
|
||||
if let index = components.count == 1 ? 0 : Session.lastOpenedComponent(checksum) {
|
||||
DispatchQueue.main.async {
|
||||
self?.navigate(to: components[index])
|
||||
}
|
||||
|
||||
if components.count == 1, let component = components.first {
|
||||
strongSelf.navigate(to: component)
|
||||
}
|
||||
|
||||
strongSelf.sections[0].items = items
|
||||
strongSelf.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
title = manifest.displayName
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
let keyboardShortcut = " (⌃⌘Z)"
|
||||
#else
|
||||
let keyboardShortcut = ""
|
||||
#endif
|
||||
sections.append(SectionData(
|
||||
items: components.enumerated().map { index, component in
|
||||
NavigationLink(title: component.displayName ?? component.appKey) { [weak self] in
|
||||
self?.navigate(to: component)
|
||||
Session.storeComponent(index: index, checksum: checksum)
|
||||
}
|
||||
},
|
||||
footer: "\(runtimeInfo())\n\nShake your device\(keyboardShortcut) to open the React Native debug menu."
|
||||
))
|
||||
|
||||
let rememberLastComponentSwitch = UISwitch()
|
||||
rememberLastComponentSwitch.isOn = Session.shouldRememberLastComponent
|
||||
rememberLastComponentSwitch.addTarget(
|
||||
self,
|
||||
action: #selector(rememberLastComponentSwitchDidChangeValue(_:)),
|
||||
for: .valueChanged
|
||||
)
|
||||
sections.append(SectionData(
|
||||
items: [
|
||||
NavigationLink(
|
||||
title: "Remember Last Opened Component",
|
||||
accessoryView: rememberLastComponentSwitch
|
||||
),
|
||||
],
|
||||
footer: nil
|
||||
))
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(scanForQRCode(_:)),
|
||||
|
@ -81,10 +119,12 @@ final class ContentViewController: UITableViewController {
|
|||
|
||||
// MARK: - UITableViewDelegate overrides
|
||||
|
||||
override public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let (_, action) = sections[indexPath.section].items[indexPath.row]
|
||||
action()
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
override public func tableView(_: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
||||
indexPath.section == Section.components
|
||||
}
|
||||
|
||||
override public func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
sections[indexPath.section].items[indexPath.row].action?()
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource overrides
|
||||
|
@ -94,16 +134,27 @@ final class ContentViewController: UITableViewController {
|
|||
}
|
||||
|
||||
override public func tableView(_: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let (title, _) = sections[indexPath.section].items[indexPath.row]
|
||||
let link = sections[indexPath.section].items[indexPath.row]
|
||||
let cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
|
||||
let presentsNewContent = indexPath.section == 0
|
||||
cell.accessoryType = presentsNewContent ? .disclosureIndicator : .none
|
||||
|
||||
if let textLabel = cell.textLabel {
|
||||
textLabel.text = title
|
||||
textLabel.textColor = presentsNewContent ? .label : .link
|
||||
textLabel.text = link.title
|
||||
textLabel.textColor = .label
|
||||
textLabel.allowsDefaultTighteningForTruncation = true
|
||||
textLabel.numberOfLines = 1
|
||||
}
|
||||
|
||||
switch indexPath.section {
|
||||
case Section.components:
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
cell.accessoryView = nil
|
||||
case Section.settings:
|
||||
cell.accessoryType = .none
|
||||
cell.accessoryView = link.accessoryView
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
|
@ -146,6 +197,10 @@ final class ContentViewController: UITableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func rememberLastComponentSwitchDidChangeValue(_ sender: UISwitch) {
|
||||
Session.shouldRememberLastComponent = sender.isOn
|
||||
}
|
||||
|
||||
private func runtimeInfo() -> String {
|
||||
let version: String = {
|
||||
guard let version = RCTGetReactNativeVersion() else {
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
// LICENSE file in the root directory of this source tree.
|
||||
//
|
||||
|
||||
import CommonCrypto
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
struct Component: Decodable {
|
||||
|
@ -50,7 +52,7 @@ struct Manifest: Decodable {
|
|||
let displayName: String
|
||||
let components: [Component]
|
||||
|
||||
static func fromFile() -> Manifest? {
|
||||
static func fromFile() -> (Manifest, String)? {
|
||||
guard let manifestURL = Bundle.main.url(forResource: "app", withExtension: "json"),
|
||||
let data = try? Data(contentsOf: manifestURL, options: .uncached)
|
||||
else {
|
||||
|
@ -59,8 +61,12 @@ struct Manifest: Decodable {
|
|||
return from(data: data)
|
||||
}
|
||||
|
||||
static func from(data: Data) -> Manifest? {
|
||||
try? JSONDecoder().decode(self, from: data)
|
||||
static func from(data: Data) -> (Manifest, String)? {
|
||||
guard let manifest = try? JSONDecoder().decode(self, from: data) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (manifest, data.sha256)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,6 +120,24 @@ extension Array where Element == Any {
|
|||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
var sha256: String {
|
||||
guard #available(iOS 13.0, macOS 10.15, *) else {
|
||||
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
_ = withUnsafeBytes {
|
||||
CC_SHA256($0.baseAddress, UInt32(count), &digest)
|
||||
}
|
||||
|
||||
return digest.reduce("") { str, byte in
|
||||
str + String(format: "%02x", UInt8(byte))
|
||||
}
|
||||
}
|
||||
|
||||
let digest = SHA256.hash(data: self)
|
||||
return Array(digest.makeIterator()).map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
extension Dictionary where Key == AnyHashable, Value == Any {
|
||||
static func decode(from decoder: Decoder) throws -> Dictionary? {
|
||||
guard let container = try? decoder.container(keyedBy: DynamicCodingKey.self) else {
|
||||
|
|
|
@ -20,7 +20,7 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
|
|||
|
||||
var remoteBundleURL: URL? {
|
||||
didSet {
|
||||
initReact(onDidInitialize: { _ in /* noop */ })
|
||||
initReact(onDidInitialize: { /* noop */ })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
|
|||
assert(forTestingPurposesOnly)
|
||||
}
|
||||
|
||||
func initReact(onDidInitialize: @escaping ([Component]) -> Void) {
|
||||
func initReact(onDidInitialize: @escaping () -> Void) {
|
||||
if let bridge = bridge {
|
||||
if remoteBundleURL == nil {
|
||||
// When loading the embedded bundle, we must disable remote
|
||||
|
@ -113,10 +113,7 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
|
|||
object: bridge
|
||||
)
|
||||
|
||||
guard let manifest = Manifest.fromFile() else {
|
||||
return
|
||||
}
|
||||
onDidInitialize(manifest.components)
|
||||
onDidInitialize()
|
||||
} else {
|
||||
assertionFailure("Failed to instantiate RCTBridge")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//
|
||||
// This source code is licensed under the MIT license found in the
|
||||
// LICENSE file in the root directory of this source tree.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Session {
|
||||
private enum Keys {
|
||||
static let checksum = "ManifestChecksum"
|
||||
static let rememberLastComponentEnabled = "RememberLastComponent/Enabled"
|
||||
static let rememberLastComponentIndex = "RememberLastComponent/Index"
|
||||
}
|
||||
|
||||
private static var lastComponentIndex: Int {
|
||||
get { UserDefaults.standard.integer(forKey: Keys.rememberLastComponentIndex) }
|
||||
set { UserDefaults.standard.set(newValue, forKey: Keys.rememberLastComponentIndex) }
|
||||
}
|
||||
|
||||
private static var manifestChecksum: String? {
|
||||
get { UserDefaults.standard.string(forKey: Keys.checksum) }
|
||||
set { UserDefaults.standard.set(newValue, forKey: Keys.checksum) }
|
||||
}
|
||||
|
||||
static var shouldRememberLastComponent: Bool {
|
||||
get { UserDefaults.standard.bool(forKey: Keys.rememberLastComponentEnabled) }
|
||||
set { UserDefaults.standard.set(newValue, forKey: Keys.rememberLastComponentEnabled) }
|
||||
}
|
||||
|
||||
static func lastOpenedComponent(_ checksum: String) -> Int? {
|
||||
guard shouldRememberLastComponent, checksum == manifestChecksum else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return lastComponentIndex
|
||||
}
|
||||
|
||||
static func storeComponent(index: Int, checksum: String) {
|
||||
lastComponentIndex = index
|
||||
manifestChecksum = checksum
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
193EF063247A736200BE8C79 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF062247A736200BE8C79 /* AppDelegate.swift */; };
|
||||
193EF08F247A799D00BE8C79 /* Manifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF08E247A799D00BE8C79 /* Manifest.swift */; };
|
||||
193EF098247B130700BE8C79 /* ReactInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF097247B130700BE8C79 /* ReactInstance.swift */; };
|
||||
1960F339258C97C400AEC7A2 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1960F338258C97C400AEC7A2 /* Session.swift */; };
|
||||
193EF065247A736200BE8C79 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF064247A736200BE8C79 /* ViewController.swift */; };
|
||||
196C22652490CBAB00449D3C /* React+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 196C22632490CBAB00449D3C /* React+Compatibility.m */; };
|
||||
193EF093247A830200BE8C79 /* UIViewController+ReactTestApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 193EF091247A830200BE8C79 /* UIViewController+ReactTestApp.m */; };
|
||||
|
@ -39,27 +40,28 @@
|
|||
/* Begin PBXFileReference section */
|
||||
193EF05F247A736200BE8C79 /* ReactTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
193EF062247A736200BE8C79 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
193EF064247A736200BE8C79 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
193EF066247A736300BE8C79 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
193EF069247A736300BE8C79 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
193EF06B247A736300BE8C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
193EF06C247A736300BE8C79 /* ReactTestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ReactTestApp.entitlements; sourceTree = "<group>"; };
|
||||
193EF071247A736300BE8C79 /* ReactTestAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactTestAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
193EF077247A736300BE8C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
193EF07C247A736300BE8C79 /* ReactTestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactTestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
193EF082247A736300BE8C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
193EF08E247A799D00BE8C79 /* Manifest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Manifest.swift; path = ../ReactTestAppShared/Manifest.swift; sourceTree = "<group>"; };
|
||||
193EF091247A830200BE8C79 /* UIViewController+ReactTestApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIViewController+ReactTestApp.m"; path = "../ReactTestAppShared/UIViewController+ReactTestApp.m"; sourceTree = "<group>"; };
|
||||
193EF092247A830200BE8C79 /* UIViewController+ReactTestApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+ReactTestApp.h"; path = "../ReactTestAppShared/UIViewController+ReactTestApp.h"; sourceTree = "<group>"; };
|
||||
193EF094247A84DA00BE8C79 /* ReactTestApp-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ReactTestApp-Bridging-Header.h"; path = "../ReactTestAppShared/ReactTestApp-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
193EF097247B130700BE8C79 /* ReactInstance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ReactInstance.swift; path = ../ReactTestAppShared/ReactInstance.swift; sourceTree = "<group>"; };
|
||||
196C22632490CBAB00449D3C /* React+Compatibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "React+Compatibility.m"; path = "../ReactTestAppShared/React+Compatibility.m"; sourceTree = "<group>"; };
|
||||
1960F338258C97C400AEC7A2 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Session.swift; path = ../ReactTestAppShared/Session.swift; sourceTree = "<group>"; };
|
||||
193EF064247A736200BE8C79 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
193EF094247A84DA00BE8C79 /* ReactTestApp-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ReactTestApp-Bridging-Header.h"; path = "../ReactTestAppShared/ReactTestApp-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
196C22642490CBAB00449D3C /* React+Compatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "React+Compatibility.h"; path = "../ReactTestAppShared/React+Compatibility.h"; sourceTree = "<group>"; };
|
||||
196C22632490CBAB00449D3C /* React+Compatibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "React+Compatibility.m"; path = "../ReactTestAppShared/React+Compatibility.m"; sourceTree = "<group>"; };
|
||||
193EF092247A830200BE8C79 /* UIViewController+ReactTestApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+ReactTestApp.h"; path = "../ReactTestAppShared/UIViewController+ReactTestApp.h"; sourceTree = "<group>"; };
|
||||
193EF091247A830200BE8C79 /* UIViewController+ReactTestApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIViewController+ReactTestApp.m"; path = "../ReactTestAppShared/UIViewController+ReactTestApp.m"; sourceTree = "<group>"; };
|
||||
193EF066247A736300BE8C79 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
193EF06C247A736300BE8C79 /* ReactTestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ReactTestApp.entitlements; sourceTree = "<group>"; };
|
||||
19B368BC24B12C24002CCEFF /* ReactTestApp.common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.common.xcconfig; sourceTree = "<group>"; };
|
||||
19B368BD24B12C24002CCEFF /* ReactTestApp.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.debug.xcconfig; sourceTree = "<group>"; };
|
||||
19B368BE24B12C24002CCEFF /* ReactTestApp.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.release.xcconfig; sourceTree = "<group>"; };
|
||||
193EF069247A736300BE8C79 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
193EF06B247A736300BE8C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
193EF071247A736300BE8C79 /* ReactTestAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactTestAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
19E791BF24B08E1400FA6468 /* ReactTestAppTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactTestAppTests.swift; sourceTree = "<group>"; };
|
||||
193EF077247A736300BE8C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
193EF07C247A736300BE8C79 /* ReactTestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactTestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
19E791C224B08E4D00FA6468 /* ReactTestAppUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactTestAppUITests.swift; sourceTree = "<group>"; };
|
||||
193EF082247A736300BE8C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -113,6 +115,7 @@
|
|||
193EF062247A736200BE8C79 /* AppDelegate.swift */,
|
||||
193EF08E247A799D00BE8C79 /* Manifest.swift */,
|
||||
193EF097247B130700BE8C79 /* ReactInstance.swift */,
|
||||
1960F338258C97C400AEC7A2 /* Session.swift */,
|
||||
193EF064247A736200BE8C79 /* ViewController.swift */,
|
||||
193EF094247A84DA00BE8C79 /* ReactTestApp-Bridging-Header.h */,
|
||||
196C22642490CBAB00449D3C /* React+Compatibility.h */,
|
||||
|
@ -307,6 +310,7 @@
|
|||
193EF08F247A799D00BE8C79 /* Manifest.swift in Sources */,
|
||||
196C22652490CBAB00449D3C /* React+Compatibility.m in Sources */,
|
||||
193EF098247B130700BE8C79 /* ReactInstance.swift in Sources */,
|
||||
1960F339258C97C400AEC7A2 /* Session.swift in Sources */,
|
||||
193EF065247A736200BE8C79 /* ViewController.swift in Sources */,
|
||||
193EF093247A830200BE8C79 /* UIViewController+ReactTestApp.m in Sources */,
|
||||
);
|
||||
|
|
|
@ -10,10 +10,12 @@ import Cocoa
|
|||
@NSApplicationMain
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@IBOutlet var reactMenu: NSMenu!
|
||||
@IBOutlet var rememberLastComponentMenuItem: NSMenuItem!
|
||||
|
||||
private(set) lazy var reactInstance = ReactInstance()
|
||||
|
||||
private weak var mainWindow: NSWindow?
|
||||
private var manifestChecksum: String?
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
|
||||
true
|
||||
|
@ -25,7 +27,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
let windows = NSApplication.shared.windows
|
||||
mainWindow = windows.first { $0.identifier?.rawValue == "MainWindow" }
|
||||
|
||||
guard let manifest = Manifest.fromFile() else {
|
||||
if Session.shouldRememberLastComponent {
|
||||
rememberLastComponentMenuItem.state = .on
|
||||
}
|
||||
|
||||
guard let (manifest, checksum) = Manifest.fromFile() else {
|
||||
let item = reactMenu.addItem(
|
||||
withTitle: "Could not load 'app.json'",
|
||||
action: nil,
|
||||
|
@ -35,28 +41,37 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
return
|
||||
}
|
||||
|
||||
for (index, component) in manifest.components.enumerated() {
|
||||
manifest.components.enumerated().forEach { index, component in
|
||||
let title = component.displayName ?? component.appKey
|
||||
let item = reactMenu.addItem(
|
||||
withTitle: title,
|
||||
action: #selector(onComponentSelected),
|
||||
keyEquivalent: index < 9 ? String(index + 1) : ""
|
||||
)
|
||||
item.tag = index
|
||||
item.keyEquivalentModifierMask = [.shift, .command]
|
||||
item.isEnabled = false
|
||||
item.representedObject = component
|
||||
}
|
||||
|
||||
let components = manifest.components
|
||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||
self?.reactInstance.initReact { _ in
|
||||
self?.reactInstance.initReact {
|
||||
DispatchQueue.main.async {
|
||||
if manifest.components.count == 1 {
|
||||
self?.present(manifest.components[0])
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
self?.reactMenu.items.forEach { $0.isEnabled = true }
|
||||
|
||||
if let index = components.count == 1 ? 0 : Session.lastOpenedComponent(checksum) {
|
||||
strongSelf.present(components[index])
|
||||
}
|
||||
|
||||
strongSelf.reactMenu.items.forEach { $0.isEnabled = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manifestChecksum = checksum
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_: Notification) {
|
||||
|
@ -72,18 +87,36 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
}
|
||||
|
||||
present(component)
|
||||
|
||||
if let checksum = manifestChecksum {
|
||||
Session.storeComponent(index: menuItem.tag, checksum: checksum)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction
|
||||
func onLoadEmbeddedBundle(_: NSMenuItem) {
|
||||
func onLoadEmbeddedBundleSelected(_: NSMenuItem) {
|
||||
reactInstance.remoteBundleURL = nil
|
||||
}
|
||||
|
||||
@IBAction
|
||||
func onLoadFromDevServer(_: NSMenuItem) {
|
||||
func onLoadFromDevServerSelected(_: NSMenuItem) {
|
||||
reactInstance.remoteBundleURL = ReactInstance.jsBundleURL()
|
||||
}
|
||||
|
||||
@IBAction
|
||||
func onRememberLastComponentSelected(_ menuItem: NSMenuItem) {
|
||||
switch menuItem.state {
|
||||
case .mixed, .on:
|
||||
Session.shouldRememberLastComponent = false
|
||||
menuItem.state = .off
|
||||
case .off:
|
||||
Session.shouldRememberLastComponent = true
|
||||
menuItem.state = .on
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private enum WindowSize {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17156"/>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17506"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
|
@ -376,13 +377,19 @@
|
|||
<menuItem title="Load Embedded JS Bundle" id="eKz-kn-Unx">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="onLoadEmbeddedBundle:" target="Voe-Tx-rLC" id="Dp5-bz-UTv"/>
|
||||
<action selector="onLoadEmbeddedBundleSelected:" target="Voe-Tx-rLC" id="Dp5-bz-UTv"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Load From Dev Server" id="175-aB-GZg">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="onLoadFromDevServer:" target="Voe-Tx-rLC" id="FOl-Nq-cWc"/>
|
||||
<action selector="onLoadFromDevServerSelected:" target="Voe-Tx-rLC" id="FOl-Nq-cWc"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Remember Last Opened Component" id="lzS-xk-P1D">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="onRememberLastComponentSelected:" target="Voe-Tx-rLC" id="TFw-gh-YQX"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="uJa-x1-YNS"/>
|
||||
|
@ -435,6 +442,7 @@
|
|||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="ReactTestApp" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="reactMenu" destination="nwF-AT-VY9" id="mZ4-MI-fvy"/>
|
||||
<outlet property="rememberLastComponentMenuItem" destination="lzS-xk-P1D" id="azf-vw-Bvd"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
|
|
|
@ -13,8 +13,10 @@
|
|||
#include <winrt/Windows.UI.ViewManagement.h>
|
||||
|
||||
#include "MainPage.g.cpp"
|
||||
#include "Session.h"
|
||||
|
||||
using ReactTestApp::JSBundleSource;
|
||||
using ReactTestApp::Session;
|
||||
using winrt::Microsoft::ReactNative::IJSValueWriter;
|
||||
using winrt::ReactTestApp::implementation::MainPage;
|
||||
using winrt::Windows::ApplicationModel::Core::CoreApplication;
|
||||
|
@ -28,6 +30,7 @@ using winrt::Windows::UI::ViewManagement::ApplicationView;
|
|||
using winrt::Windows::UI::Xaml::RoutedEventArgs;
|
||||
using winrt::Windows::UI::Xaml::Window;
|
||||
using winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem;
|
||||
using winrt::Windows::UI::Xaml::Controls::ToggleMenuFlyoutItem;
|
||||
using winrt::Windows::UI::Xaml::Input::KeyboardAccelerator;
|
||||
using winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs;
|
||||
|
||||
|
@ -143,6 +146,12 @@ void MainPage::LoadFromJSBundle(IInspectable const &, RoutedEventArgs)
|
|||
LoadJSBundleFrom(JSBundleSource::Embedded);
|
||||
}
|
||||
|
||||
void MainPage::ToggleRememberLastComponent(IInspectable const &sender, RoutedEventArgs)
|
||||
{
|
||||
auto item = sender.as<ToggleMenuFlyoutItem>();
|
||||
Session::ShouldRememberLastComponent(item.IsChecked());
|
||||
}
|
||||
|
||||
void MainPage::Reload(Windows::Foundation::IInspectable const &, Windows::UI::Xaml::RoutedEventArgs)
|
||||
{
|
||||
reactInstance_.Reload();
|
||||
|
@ -238,51 +247,64 @@ void MainPage::InitializeDebugMenu()
|
|||
|
||||
void MainPage::InitializeReactMenu()
|
||||
{
|
||||
std::optional<::ReactTestApp::Manifest> manifest = ::ReactTestApp::GetManifest("app.json");
|
||||
RememberLastComponentMenuItem().IsChecked(Session::ShouldRememberLastComponent());
|
||||
|
||||
auto result = ::ReactTestApp::GetManifest("app.json");
|
||||
auto menuItems = ReactMenuBarItem().Items();
|
||||
if (!manifest.has_value()) {
|
||||
if (!result.has_value()) {
|
||||
MenuFlyoutItem newMenuItem;
|
||||
newMenuItem.Text(L"Couldn't parse 'app.json'");
|
||||
newMenuItem.IsEnabled(false);
|
||||
menuItems.Append(newMenuItem);
|
||||
return;
|
||||
}
|
||||
|
||||
auto &&[manifest, checksum] = result.value();
|
||||
manifestChecksum_ = std::move(checksum);
|
||||
|
||||
// If only one component is present load, it automatically. Otherwise, check
|
||||
// whether we can reopen a component from previous session.
|
||||
auto &components = manifest.components;
|
||||
auto index = components.size() == 1 ? 0 : Session::GetLastOpenedComponent(manifestChecksum_);
|
||||
if (index.has_value()) {
|
||||
Loaded([this, component = components[index.value()]](IInspectable const &,
|
||||
RoutedEventArgs const &) {
|
||||
LoadReactComponent(component);
|
||||
});
|
||||
} else {
|
||||
// If only one component is present load it automatically
|
||||
auto &components = manifest.value().components;
|
||||
if (components.size() == 1) {
|
||||
LoadReactComponent(components.at(0));
|
||||
} else {
|
||||
AppTitle().Text(to_hstring(manifest.value().displayName));
|
||||
AppTitle().Text(to_hstring(manifest.displayName));
|
||||
}
|
||||
|
||||
auto keyboardAcceleratorKey = VirtualKey::Number1;
|
||||
for (int i = 0; i < static_cast<int>(components.size()); ++i) {
|
||||
auto &&component = components[i];
|
||||
|
||||
MenuFlyoutItem newMenuItem;
|
||||
newMenuItem.Text(winrt::to_hstring(component.displayName.value_or(component.appKey)));
|
||||
newMenuItem.Click(
|
||||
[this, component = std::move(component), i](IInspectable const &, RoutedEventArgs) {
|
||||
LoadReactComponent(component);
|
||||
Session::StoreComponent(i, manifestChecksum_);
|
||||
});
|
||||
|
||||
// Add keyboard accelerator for first nine (1-9) components
|
||||
if (keyboardAcceleratorKey <= VirtualKey::Number9) {
|
||||
auto const num = std::underlying_type_t<VirtualKey>(keyboardAcceleratorKey) -
|
||||
std::underlying_type_t<VirtualKey>(VirtualKey::Number0);
|
||||
newMenuItem.AccessKey(to_hstring(num));
|
||||
|
||||
KeyboardAccelerator keyboardAccelerator;
|
||||
keyboardAccelerator.Modifiers(VirtualKeyModifiers::Control |
|
||||
VirtualKeyModifiers::Shift);
|
||||
keyboardAccelerator.Key(keyboardAcceleratorKey);
|
||||
|
||||
newMenuItem.KeyboardAccelerators().Append(keyboardAccelerator);
|
||||
|
||||
keyboardAcceleratorKey =
|
||||
static_cast<VirtualKey>(static_cast<int32_t>(keyboardAcceleratorKey) + 1);
|
||||
}
|
||||
|
||||
auto keyboardAcceleratorKey = VirtualKey::Number1;
|
||||
for (auto &&c : components) {
|
||||
MenuFlyoutItem newMenuItem;
|
||||
|
||||
newMenuItem.Text(winrt::to_hstring(c.displayName.value_or(c.appKey)));
|
||||
newMenuItem.Click(
|
||||
[this, component = std::move(c)](IInspectable const &, RoutedEventArgs) {
|
||||
LoadReactComponent(component);
|
||||
});
|
||||
|
||||
// Add keyboard accelerators for first nine (1-9) components
|
||||
if (keyboardAcceleratorKey <= VirtualKey::Number9) {
|
||||
auto const num = std::underlying_type_t<VirtualKey>(keyboardAcceleratorKey) -
|
||||
std::underlying_type_t<VirtualKey>(VirtualKey::Number0);
|
||||
newMenuItem.AccessKey(to_hstring(num));
|
||||
|
||||
KeyboardAccelerator keyboardAccelerator;
|
||||
keyboardAccelerator.Modifiers(VirtualKeyModifiers::Control |
|
||||
VirtualKeyModifiers::Shift);
|
||||
keyboardAccelerator.Key(keyboardAcceleratorKey);
|
||||
|
||||
newMenuItem.KeyboardAccelerators().Append(keyboardAccelerator);
|
||||
|
||||
keyboardAcceleratorKey =
|
||||
static_cast<VirtualKey>(static_cast<int32_t>(keyboardAcceleratorKey) + 1);
|
||||
}
|
||||
|
||||
menuItems.Append(newMenuItem);
|
||||
}
|
||||
menuItems.Append(newMenuItem);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,10 +22,16 @@ namespace winrt::ReactTestApp::implementation
|
|||
public:
|
||||
MainPage();
|
||||
|
||||
// React menu
|
||||
|
||||
void LoadFromDevServer(Windows::Foundation::IInspectable const &,
|
||||
Windows::UI::Xaml::RoutedEventArgs);
|
||||
void LoadFromJSBundle(Windows::Foundation::IInspectable const &,
|
||||
Windows::UI::Xaml::RoutedEventArgs);
|
||||
void ToggleRememberLastComponent(Windows::Foundation::IInspectable const &,
|
||||
Windows::UI::Xaml::RoutedEventArgs);
|
||||
|
||||
// Debug menu
|
||||
|
||||
void Reload(Windows::Foundation::IInspectable const &, Windows::UI::Xaml::RoutedEventArgs);
|
||||
void ToggleBreakOnFirstLine(Windows::Foundation::IInspectable const &,
|
||||
|
@ -46,6 +52,7 @@ namespace winrt::ReactTestApp::implementation
|
|||
using Base = MainPageT;
|
||||
|
||||
::ReactTestApp::ReactInstance reactInstance_;
|
||||
std::string manifestChecksum_;
|
||||
|
||||
void InitializeDebugMenu();
|
||||
void InitializeReactMenu();
|
||||
|
|
|
@ -21,6 +21,12 @@
|
|||
<MenuBarItem x:Name="ReactMenuBarItem" Title="React" AccessKey="R">
|
||||
<MenuFlyoutItem Text="Load from JS bundle" Click="LoadFromJSBundle" AccessKey="E"/>
|
||||
<MenuFlyoutItem Text="Load from dev server" Click="LoadFromDevServer" AccessKey="D"/>
|
||||
<ToggleMenuFlyoutItem
|
||||
x:Name="RememberLastComponentMenuItem"
|
||||
Text="Remember last opened component"
|
||||
Click="ToggleRememberLastComponent"
|
||||
AccessKey="R"
|
||||
/>
|
||||
<MenuFlyoutSeparator/>
|
||||
</MenuBarItem>
|
||||
<MenuBarItem x:Name="DebugMenuBarItem" IsEnabled="false" Title="Debug" AccessKey="D">
|
||||
|
|
|
@ -13,11 +13,26 @@
|
|||
#include <iostream>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <winrt/Windows.Security.Cryptography.Core.h>
|
||||
#include <winrt/Windows.Security.Cryptography.h>
|
||||
|
||||
using nlohmann::detail::value_t;
|
||||
using winrt::Windows::Security::Cryptography::BinaryStringEncoding;
|
||||
using winrt::Windows::Security::Cryptography::CryptographicBuffer;
|
||||
using winrt::Windows::Security::Cryptography::Core::HashAlgorithmNames;
|
||||
using winrt::Windows::Security::Cryptography::Core::HashAlgorithmProvider;
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string checksum(std::string const &data)
|
||||
{
|
||||
auto hasher = HashAlgorithmProvider::OpenAlgorithm(HashAlgorithmNames::Sha256());
|
||||
auto digest = hasher.HashData(CryptographicBuffer::ConvertStringToBinary(
|
||||
winrt::to_hstring(data), BinaryStringEncoding::Utf8));
|
||||
auto checksum = CryptographicBuffer::EncodeToHexString(digest);
|
||||
return winrt::to_string(checksum);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::optional<T> get_optional(const nlohmann::json &j, const std::string &key)
|
||||
{
|
||||
|
@ -99,15 +114,24 @@ namespace ReactTestApp
|
|||
m.components = j.at("components").get<std::vector<Component>>();
|
||||
}
|
||||
|
||||
std::optional<Manifest> GetManifest(std::string const &manifestFileName)
|
||||
std::optional<std::pair<Manifest, std::string>> GetManifest(std::string const &filename)
|
||||
{
|
||||
std::ifstream manifestFile(manifestFileName);
|
||||
nlohmann::json j = nlohmann::json::parse(manifestFile, nullptr, false);
|
||||
std::string json;
|
||||
{
|
||||
std::ifstream stream(filename);
|
||||
|
||||
stream.seekg(0, std::ios::end);
|
||||
json.reserve(static_cast<size_t>(stream.tellg()));
|
||||
|
||||
stream.seekg(0, std::ios::beg);
|
||||
json.assign(std::istreambuf_iterator<char>(stream), {});
|
||||
}
|
||||
|
||||
auto j = nlohmann::json::parse(json, nullptr, false);
|
||||
if (j.is_discarded()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Manifest m = j.get<Manifest>();
|
||||
return m;
|
||||
return std::make_pair(j.get<Manifest>(), checksum(json));
|
||||
}
|
||||
} // namespace ReactTestApp
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ReactTestApp
|
||||
|
@ -28,6 +29,6 @@ namespace ReactTestApp
|
|||
std::vector<Component> components;
|
||||
};
|
||||
|
||||
std::optional<Manifest> GetManifest(std::string const &manifestFileName);
|
||||
std::optional<std::pair<Manifest, std::string>> GetManifest(std::string const &filename);
|
||||
|
||||
} // namespace ReactTestApp
|
||||
|
|
|
@ -153,6 +153,7 @@
|
|||
<ClInclude Include="$(ReactTestAppDir)\Manifest.h" />
|
||||
<ClInclude Include="$(ReactTestAppDir)\ReactInstance.h" />
|
||||
<ClInclude Include="$(ReactTestAppDir)\ReactPackageProvider.h" />
|
||||
<ClInclude Include="$(ReactTestAppDir)\Session.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="$(ReactTestAppDir)\App.xaml">
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
<ClInclude Include="$(ReactTestAppDir)\Manifest.h" />
|
||||
<ClInclude Include="$(ReactTestAppDir)\ReactInstance.h" />
|
||||
<ClInclude Include="$(ReactTestAppDir)\ReactPackageProvider.h" />
|
||||
<ClInclude Include="$(ReactTestAppDir)\Session.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Text Include="$(ProjectRootDir)\app.json">
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
//
|
||||
// Copyright (c) Microsoft Corporation
|
||||
//
|
||||
// This source code is licensed under the MIT license found in the
|
||||
// LICENSE file in the root directory of this source tree.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Storage.h>
|
||||
|
||||
using winrt::Windows::Foundation::PropertyValue;
|
||||
using winrt::Windows::Storage::ApplicationData;
|
||||
|
||||
namespace ReactTestApp
|
||||
{
|
||||
struct Session {
|
||||
private:
|
||||
static inline const std::wstring kChecksum = L"ManifestChecksum";
|
||||
static inline const std::wstring kRememberLastComponentEnabled =
|
||||
L"RememberLastComponent/Enabled";
|
||||
static inline const std::wstring kRememberLastComponentIndex =
|
||||
L"RememberLastComponent/Index";
|
||||
|
||||
static auto LocalSettings()
|
||||
{
|
||||
return ApplicationData::Current().LocalSettings().Values();
|
||||
}
|
||||
|
||||
static int LastComponentIndex()
|
||||
{
|
||||
auto index = LocalSettings().Lookup(kRememberLastComponentIndex);
|
||||
return winrt::unbox_value_or<int>(index, -1);
|
||||
}
|
||||
|
||||
static void LastComponentIndex(int value)
|
||||
{
|
||||
LocalSettings().Insert(kRememberLastComponentIndex, PropertyValue::CreateInt32(value));
|
||||
}
|
||||
|
||||
static std::string ManifestChecksum()
|
||||
{
|
||||
auto checksum = LocalSettings().Lookup(kChecksum);
|
||||
auto value = winrt::unbox_value_or<winrt::hstring>(checksum, L"");
|
||||
return winrt::to_string(value);
|
||||
}
|
||||
|
||||
static void ManifestChecksum(std::string const &value)
|
||||
{
|
||||
auto v = PropertyValue::CreateString(winrt::to_hstring(value));
|
||||
LocalSettings().Insert(kChecksum, v);
|
||||
}
|
||||
|
||||
public:
|
||||
static bool ShouldRememberLastComponent()
|
||||
{
|
||||
auto value = LocalSettings().Lookup(kRememberLastComponentEnabled);
|
||||
return winrt::unbox_value_or<bool>(value, false);
|
||||
}
|
||||
|
||||
static void ShouldRememberLastComponent(bool enable)
|
||||
{
|
||||
auto value = PropertyValue::CreateBoolean(enable);
|
||||
LocalSettings().Insert(kRememberLastComponentEnabled, value);
|
||||
}
|
||||
|
||||
static std::optional<int> GetLastOpenedComponent(std::string const &manifestChecksum)
|
||||
{
|
||||
if (!ShouldRememberLastComponent() || manifestChecksum != ManifestChecksum()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return LastComponentIndex();
|
||||
}
|
||||
|
||||
static void StoreComponent(int index, std::string const &manifestChecksum)
|
||||
{
|
||||
LastComponentIndex(index);
|
||||
ManifestChecksum(manifestChecksum);
|
||||
}
|
||||
};
|
||||
} // namespace ReactTestApp
|
Загрузка…
Ссылка в новой задаче