From be49fea386e0339421947da77d109f9c4be0f4f4 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Fri, 8 Sep 2023 11:02:44 -0400 Subject: [PATCH 1/6] Intitial work to tie in Strada support with component examples --- .../hotwire/turbo/demo/base/NavDestination.kt | 7 +++- .../turbo/demo/features/web/WebFragment.kt | 33 +++++++++++++++++++ .../hotwire/turbo/demo/main/MainActivity.kt | 2 ++ .../demo/main/MainSessionNavHostFragment.kt | 6 ++++ .../demo/strada/BridgeComponentFactories.kt | 7 ++++ .../turbo/demo/strada/FormComponent.kt | 17 ++++++++++ .../dev/hotwire/turbo/demo/util/Extensions.kt | 5 ++- 7 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/BridgeComponentFactories.kt create mode 100644 demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/FormComponent.kt diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/base/NavDestination.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/base/NavDestination.kt index 4f8a64ef..857e2a37 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/base/NavDestination.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/base/NavDestination.kt @@ -7,6 +7,7 @@ import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_ON import androidx.navigation.NavOptions import androidx.navigation.navOptions +import dev.hotwire.strada.BridgeDestination import dev.hotwire.turbo.config.TurboPathConfigurationProperties import dev.hotwire.turbo.config.context import dev.hotwire.turbo.demo.R @@ -14,7 +15,7 @@ import dev.hotwire.turbo.demo.util.BASE_URL import dev.hotwire.turbo.nav.TurboNavDestination import dev.hotwire.turbo.nav.TurboNavPresentationContext.MODAL -interface NavDestination : TurboNavDestination { +interface NavDestination : TurboNavDestination, BridgeDestination { val menuProgress: MenuItem? get() = toolbarForNavigation()?.menu?.findItem(R.id.menu_progress) @@ -38,6 +39,10 @@ interface NavDestination : TurboNavDestination { } } + override fun bridgeWebViewIsReady(): Boolean { + return session.isReady + } + private fun isNavigable(location: String): Boolean { return location.startsWith(BASE_URL) } diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt index 1637640d..a3aff4e7 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt @@ -2,19 +2,52 @@ package dev.hotwire.turbo.demo.features.web import android.os.Bundle import android.view.View +import dev.hotwire.strada.BridgeDelegate import dev.hotwire.turbo.demo.R import dev.hotwire.turbo.demo.base.NavDestination +import dev.hotwire.turbo.demo.strada.bridgeComponentFactories import dev.hotwire.turbo.demo.util.SIGN_IN_URL import dev.hotwire.turbo.fragments.TurboWebFragment import dev.hotwire.turbo.nav.TurboNavGraphDestination +import dev.hotwire.turbo.views.TurboWebView import dev.hotwire.turbo.visit.TurboVisitAction.REPLACE import dev.hotwire.turbo.visit.TurboVisitOptions @TurboNavGraphDestination(uri = "turbo://fragment/web") open class WebFragment : TurboWebFragment(), NavDestination { + private val bridgeDelegate by lazy { + BridgeDelegate( + location = location, + destination = this, + componentFactories = bridgeComponentFactories + ) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupMenu() + viewLifecycleOwner.lifecycle.addObserver(bridgeDelegate) + } + + override fun onDestroyView() { + super.onDestroyView() + viewLifecycleOwner.lifecycle.removeObserver(bridgeDelegate) + } + + override fun onColdBootPageStarted(location: String) { + bridgeDelegate.onColdBootPageStarted() + } + + override fun onColdBootPageCompleted(location: String) { + bridgeDelegate.onColdBootPageCompleted() + } + + override fun onWebViewAttached(webView: TurboWebView) { + bridgeDelegate.onWebViewAttached(webView) + } + + override fun onWebViewDetached(webView: TurboWebView) { + bridgeDelegate.onWebViewDetached() } override fun onFormSubmissionStarted(location: String) { diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainActivity.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainActivity.kt index 122ab44a..bbe0e1ff 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainActivity.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainActivity.kt @@ -3,6 +3,7 @@ package dev.hotwire.turbo.demo.main import android.os.Bundle import android.webkit.WebView import androidx.appcompat.app.AppCompatActivity +import dev.hotwire.strada.Strada import dev.hotwire.turbo.BuildConfig import dev.hotwire.turbo.activities.TurboActivity import dev.hotwire.turbo.config.Turbo @@ -23,6 +24,7 @@ class MainActivity : AppCompatActivity(), TurboActivity { private fun configApp() { if (BuildConfig.DEBUG) { Turbo.config.debugLoggingEnabled = true + Strada.config.debugLoggingEnabled = true WebView.setWebContentsDebuggingEnabled(true) } } diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainSessionNavHostFragment.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainSessionNavHostFragment.kt index 50d02ef8..8e809c69 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainSessionNavHostFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainSessionNavHostFragment.kt @@ -2,6 +2,7 @@ package dev.hotwire.turbo.demo.main import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment +import dev.hotwire.strada.Bridge import dev.hotwire.turbo.config.TurboPathConfiguration import dev.hotwire.turbo.demo.features.imageviewer.ImageViewerFragment import dev.hotwire.turbo.demo.features.numbers.NumberBottomSheetFragment @@ -43,7 +44,12 @@ class MainSessionNavHostFragment : TurboSessionNavHostFragment() { override fun onSessionCreated() { super.onSessionCreated() + + // Configure WebView session.webView.settings.userAgentString = session.webView.customUserAgent session.webView.initDayNightTheme() + + // Initialize Strada bridge + Bridge.initialize(session.webView) } } diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/BridgeComponentFactories.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/BridgeComponentFactories.kt new file mode 100644 index 00000000..2a0c2646 --- /dev/null +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/BridgeComponentFactories.kt @@ -0,0 +1,7 @@ +package dev.hotwire.turbo.demo.strada + +import dev.hotwire.strada.BridgeComponentFactory + +val bridgeComponentFactories = listOf( + BridgeComponentFactory("form", ::FormComponent) +) diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/FormComponent.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/FormComponent.kt new file mode 100644 index 00000000..9b46062c --- /dev/null +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/FormComponent.kt @@ -0,0 +1,17 @@ +package dev.hotwire.turbo.demo.strada + +import android.util.Log +import dev.hotwire.strada.BridgeComponent +import dev.hotwire.strada.BridgeDelegate +import dev.hotwire.strada.Message +import dev.hotwire.turbo.demo.base.NavDestination + +class FormComponent( + name: String, + delegate: BridgeDelegate +) : BridgeComponent(name, delegate) { + + override fun onReceive(message: Message) { + Log.d("Demo", "FormComponent message received: $message") + } +} diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/util/Extensions.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/util/Extensions.kt index a0da63e4..7cfa592e 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/util/Extensions.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/util/Extensions.kt @@ -8,9 +8,11 @@ import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat import androidx.webkit.WebSettingsCompat import androidx.webkit.WebViewFeature +import dev.hotwire.strada.Strada import dev.hotwire.turbo.config.Turbo import dev.hotwire.turbo.config.TurboPathConfigurationProperties import dev.hotwire.turbo.demo.R +import dev.hotwire.turbo.demo.strada.bridgeComponentFactories val TurboPathConfigurationProperties.description: String? get() = get("description") @@ -38,7 +40,8 @@ fun WebView.initDayNightTheme() { val WebView.customUserAgent: String get() { val turboSubstring = Turbo.userAgentSubstring() - return "$turboSubstring; ${settings.userAgentString}" + val stradaSubstring = Strada.userAgentSubstring(bridgeComponentFactories) + return "$turboSubstring; $stradaSubstring; ${settings.userAgentString}" } private fun isNightModeEnabled(context: Context): Boolean { From e6c57c5a02b9e8286e685bc08653a13af1fe53b7 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Fri, 8 Sep 2023 14:18:32 -0400 Subject: [PATCH 2/6] Add the form component implementation --- demo/build.gradle | 21 +++++- .../hotwire/turbo/demo/main/MainActivity.kt | 3 + .../turbo/demo/strada/FormComponent.kt | 75 ++++++++++++++++++- .../main/res/layout/form_component_submit.xml | 14 ++++ 4 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 demo/src/main/res/layout/form_component_submit.xml diff --git a/demo/build.gradle b/demo/build.gradle index 23fbeda3..33b40f16 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -1,5 +1,17 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-serialization:1.8.0" + } +} android { compileSdkVersion 33 @@ -13,6 +25,10 @@ android { vectorDrawables.useSupportLibrary = true } + buildFeatures { + viewBinding = true + } + buildTypes { release { minifyEnabled false @@ -49,10 +65,11 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.google.android.material:material:1.8.0' + implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.recyclerview:recyclerview:1.3.0' + implementation 'androidx.recyclerview:recyclerview:1.3.1' implementation 'androidx.browser:browser:1.5.0' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation 'com.github.bumptech.glide:glide:4.15.1' implementation project(':turbo') diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainActivity.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainActivity.kt index bbe0e1ff..b8c4ce84 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainActivity.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainActivity.kt @@ -3,6 +3,7 @@ package dev.hotwire.turbo.demo.main import android.os.Bundle import android.webkit.WebView import androidx.appcompat.app.AppCompatActivity +import dev.hotwire.strada.KotlinXJsonConverter import dev.hotwire.strada.Strada import dev.hotwire.turbo.BuildConfig import dev.hotwire.turbo.activities.TurboActivity @@ -22,6 +23,8 @@ class MainActivity : AppCompatActivity(), TurboActivity { } private fun configApp() { + Strada.config.jsonConverter = KotlinXJsonConverter() + if (BuildConfig.DEBUG) { Turbo.config.debugLoggingEnabled = true Strada.config.debugLoggingEnabled = true diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/FormComponent.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/FormComponent.kt index 9b46062c..489f2fa4 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/FormComponent.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/FormComponent.kt @@ -1,17 +1,88 @@ package dev.hotwire.turbo.demo.strada import android.util.Log +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment import dev.hotwire.strada.BridgeComponent import dev.hotwire.strada.BridgeDelegate import dev.hotwire.strada.Message +import dev.hotwire.turbo.demo.R import dev.hotwire.turbo.demo.base.NavDestination +import dev.hotwire.turbo.demo.databinding.FormComponentSubmitBinding +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable class FormComponent( name: String, - delegate: BridgeDelegate + private val delegate: BridgeDelegate ) : BridgeComponent(name, delegate) { + private val submitButtonItemId = 37 + private var submitMenuItem: MenuItem? = null + private val fragment: Fragment + get() = delegate.destination.fragment + private val toolbar: Toolbar? + get() = fragment.view?.findViewById(R.id.toolbar) + override fun onReceive(message: Message) { - Log.d("Demo", "FormComponent message received: $message") + when (message.event) { + "connect" -> handleConnectEvent(message) + "submitEnabled" -> handleSubmitEnabled() + "submitDisabled" -> handleSubmitDisabled() + else -> Log.w("TurboDemo", "Unknown event for message: $message") + } + } + + private fun handleConnectEvent(message: Message) { + val data = message.data() ?: return + showToolbarButton(data) + } + + private fun handleSubmitEnabled() { + toggleSubmitButton(true) } + + private fun handleSubmitDisabled() { + toggleSubmitButton(false) + } + + private fun showToolbarButton(data: MessageData) { + val menu = toolbar?.menu ?: return + val inflater = LayoutInflater.from(fragment.requireContext()) + val binding = FormComponentSubmitBinding.inflate(inflater) + val order = 999 // Show as the right-most button + + binding.formSubmit.apply { + text = data.title + setOnClickListener { + performSubmit() + } + } + + menu.removeItem(submitButtonItemId) + submitMenuItem = menu.add(Menu.NONE, submitButtonItemId, order, data.title).apply { + actionView = binding.root + setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + } + } + + private fun toggleSubmitButton(enable: Boolean) { + val layout = submitMenuItem?.actionView ?: return + + FormComponentSubmitBinding.bind(layout).apply { + formSubmit.isEnabled = enable + } + } + + private fun performSubmit(): Boolean { + return replyTo("connect") + } + + @Serializable + data class MessageData( + @SerialName("submitTitle") val title: String + ) } diff --git a/demo/src/main/res/layout/form_component_submit.xml b/demo/src/main/res/layout/form_component_submit.xml new file mode 100644 index 00000000..c8cacec4 --- /dev/null +++ b/demo/src/main/res/layout/form_component_submit.xml @@ -0,0 +1,14 @@ + + + + + + From 798227f9ef3cd46aa297ac41de8794e74fc68da0 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Fri, 8 Sep 2023 16:28:26 -0400 Subject: [PATCH 3/6] Configure the strada form path as a modal screen --- demo/src/main/assets/json/configuration.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/src/main/assets/json/configuration.json b/demo/src/main/assets/json/configuration.json index abc40df1..6436ba90 100644 --- a/demo/src/main/assets/json/configuration.json +++ b/demo/src/main/assets/json/configuration.json @@ -26,7 +26,8 @@ }, { "patterns": [ - "/signin$" + "/signin$", + "/strada-form$" ], "properties": { "context": "modal", From 5b1835b08f184c8298868082e0d0ff4785ff1920 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Mon, 11 Sep 2023 12:10:28 -0400 Subject: [PATCH 4/6] Add the menu component implementation --- .../demo/features/numbers/NumbersAdapter.kt | 2 + .../demo/strada/BridgeComponentFactories.kt | 3 +- .../turbo/demo/strada/FormComponent.kt | 4 + .../turbo/demo/strada/MenuComponent.kt | 84 +++++++++++++++++++ .../turbo/demo/strada/MenuComponentAdapter.kt | 57 +++++++++++++ .../res/layout/menu_component_adapter_row.xml | 22 +++++ .../layout/menu_component_bottom_sheet.xml | 33 ++++++++ 7 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/MenuComponent.kt create mode 100644 demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/MenuComponentAdapter.kt create mode 100644 demo/src/main/res/layout/menu_component_adapter_row.xml create mode 100644 demo/src/main/res/layout/menu_component_bottom_sheet.xml diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/numbers/NumbersAdapter.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/numbers/NumbersAdapter.kt index 85f7d687..0ba21a54 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/numbers/NumbersAdapter.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/numbers/NumbersAdapter.kt @@ -1,5 +1,6 @@ package dev.hotwire.turbo.demo.features.numbers +import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -11,6 +12,7 @@ class NumbersAdapter(val callback: NumbersFragmentCallback) : RecyclerView.Adapt private val type = R.layout.adapter_numbers_row private var items = emptyList() + @SuppressLint("NotifyDataSetChanged") set(value) { field = value notifyDataSetChanged() diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/BridgeComponentFactories.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/BridgeComponentFactories.kt index 2a0c2646..f230956a 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/BridgeComponentFactories.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/BridgeComponentFactories.kt @@ -3,5 +3,6 @@ package dev.hotwire.turbo.demo.strada import dev.hotwire.strada.BridgeComponentFactory val bridgeComponentFactories = listOf( - BridgeComponentFactory("form", ::FormComponent) + BridgeComponentFactory("form", ::FormComponent), + BridgeComponentFactory("menu", ::MenuComponent) ) diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/FormComponent.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/FormComponent.kt index 489f2fa4..8ba26118 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/FormComponent.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/FormComponent.kt @@ -15,6 +15,10 @@ import dev.hotwire.turbo.demo.databinding.FormComponentSubmitBinding import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +/** + * Bridge component to display a submit button in the native toolbar, + * which will submit the form on the page when tapped. + */ class FormComponent( name: String, private val delegate: BridgeDelegate diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/MenuComponent.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/MenuComponent.kt new file mode 100644 index 00000000..24ee24c8 --- /dev/null +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/MenuComponent.kt @@ -0,0 +1,84 @@ +package dev.hotwire.turbo.demo.strada + +import android.util.Log +import android.view.LayoutInflater +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetDialog +import dev.hotwire.strada.BridgeComponent +import dev.hotwire.strada.BridgeDelegate +import dev.hotwire.strada.Message +import dev.hotwire.turbo.demo.R +import dev.hotwire.turbo.demo.base.NavDestination +import dev.hotwire.turbo.demo.databinding.MenuComponentBottomSheetBinding +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Bridge component to display a native bottom sheet menu, which will + * send the selected index of the tapped menu item back to the web. + */ +class MenuComponent( + name: String, + private val delegate: BridgeDelegate +) : BridgeComponent(name, delegate) { + + private val fragment: Fragment + get() = delegate.destination.fragment + + override fun onReceive(message: Message) { + when (message.event) { + "display" -> handleDisplayEvent(message) + else -> Log.w("TurboDemo", "Unknown event for message: $message") + } + } + + private fun handleDisplayEvent(message: Message) { + val data = message.data() ?: return + showBottomSheet(data.title, data.items) + } + + private fun showBottomSheet(title: String, items: List) { + val view = fragment.view?.rootView ?: return + val inflater = LayoutInflater.from(view.context) + val bottomSheet = BottomSheetDialog(view.context) + val binding = MenuComponentBottomSheetBinding.inflate(inflater) + + binding.toolbar.title = title + binding.recyclerView.layoutManager = LinearLayoutManager(view.context) + binding.recyclerView.adapter = MenuComponentAdapter().apply { + setData(items) + setListener { + bottomSheet.dismiss() + onItemSelected(it) + } + } + + bottomSheet.apply { + setContentView(binding.root) + show() + } + } + + private fun onItemSelected(item: Item) { + replyTo("display", SelectionMessageData(item.index)) + } + + @Serializable + data class MessageData( + @SerialName("title") val title: String, + @SerialName("items") val items: List + ) + + @Serializable + data class Item( + @SerialName("title") val title: String, + @SerialName("index") val index: Int + ) + + @Serializable + data class SelectionMessageData( + @SerialName("selectedIndex") val selectedIndex: Int + ) +} diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/MenuComponentAdapter.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/MenuComponentAdapter.kt new file mode 100644 index 00000000..cc33a533 --- /dev/null +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/MenuComponentAdapter.kt @@ -0,0 +1,57 @@ +package dev.hotwire.turbo.demo.strada + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.textview.MaterialTextView +import dev.hotwire.turbo.demo.R + +class MenuComponentAdapter : RecyclerView.Adapter() { + private val type = R.layout.menu_component_adapter_row + private var action: ((MenuComponent.Item) -> Unit)? = null + + private var items = emptyList() + @SuppressLint("NotifyDataSetChanged") + set(value) { + field = value + notifyDataSetChanged() + } + + fun setData(items: List) { + this.items = items + } + + fun setListener(action: (item: MenuComponent.Item) -> Unit) { + this.action = action + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount(): Int { + return items.count() + } + + override fun getItemViewType(position: Int): Int { + return type + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) + return ViewHolder(view) + } + + inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + private val textView: MaterialTextView = view.findViewById(R.id.title) + + fun bind(item: MenuComponent.Item) { + textView.text = item.title + itemView.setOnClickListener { + action?.invoke(item) + } + } + } +} diff --git a/demo/src/main/res/layout/menu_component_adapter_row.xml b/demo/src/main/res/layout/menu_component_adapter_row.xml new file mode 100644 index 00000000..64f8aa6b --- /dev/null +++ b/demo/src/main/res/layout/menu_component_adapter_row.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/demo/src/main/res/layout/menu_component_bottom_sheet.xml b/demo/src/main/res/layout/menu_component_bottom_sheet.xml new file mode 100644 index 00000000..e52af604 --- /dev/null +++ b/demo/src/main/res/layout/menu_component_bottom_sheet.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + From e554a93a6ac7b4edc83914c3d9c42783537c0d7c Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Mon, 11 Sep 2023 12:48:12 -0400 Subject: [PATCH 5/6] Add the overflow-menu component implementation --- .../demo/main/MainSessionNavHostFragment.kt | 2 +- .../demo/strada/BridgeComponentFactories.kt | 3 +- .../turbo/demo/strada/MenuComponent.kt | 2 - .../demo/strada/OverflowMenuComponent.kt | 67 +++++++++++++++++++ demo/src/main/res/drawable/ic_overflow.xml | 9 +++ demo/src/main/res/menu/web.xml | 9 +++ 6 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/OverflowMenuComponent.kt create mode 100644 demo/src/main/res/drawable/ic_overflow.xml diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainSessionNavHostFragment.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainSessionNavHostFragment.kt index 8e809c69..0e470dc0 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainSessionNavHostFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainSessionNavHostFragment.kt @@ -49,7 +49,7 @@ class MainSessionNavHostFragment : TurboSessionNavHostFragment() { session.webView.settings.userAgentString = session.webView.customUserAgent session.webView.initDayNightTheme() - // Initialize Strada bridge + // Initialize Strada bridge with new WebView instance Bridge.initialize(session.webView) } } diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/BridgeComponentFactories.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/BridgeComponentFactories.kt index f230956a..b65499fe 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/BridgeComponentFactories.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/BridgeComponentFactories.kt @@ -4,5 +4,6 @@ import dev.hotwire.strada.BridgeComponentFactory val bridgeComponentFactories = listOf( BridgeComponentFactory("form", ::FormComponent), - BridgeComponentFactory("menu", ::MenuComponent) + BridgeComponentFactory("menu", ::MenuComponent), + BridgeComponentFactory("overflow-menu", ::OverflowMenuComponent) ) diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/MenuComponent.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/MenuComponent.kt index 24ee24c8..5749e64b 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/MenuComponent.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/MenuComponent.kt @@ -2,14 +2,12 @@ package dev.hotwire.turbo.demo.strada import android.util.Log import android.view.LayoutInflater -import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.bottomsheet.BottomSheetDialog import dev.hotwire.strada.BridgeComponent import dev.hotwire.strada.BridgeDelegate import dev.hotwire.strada.Message -import dev.hotwire.turbo.demo.R import dev.hotwire.turbo.demo.base.NavDestination import dev.hotwire.turbo.demo.databinding.MenuComponentBottomSheetBinding import kotlinx.serialization.SerialName diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/OverflowMenuComponent.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/OverflowMenuComponent.kt new file mode 100644 index 00000000..13649c14 --- /dev/null +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/strada/OverflowMenuComponent.kt @@ -0,0 +1,67 @@ +package dev.hotwire.turbo.demo.strada + +import android.util.Log +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment +import dev.hotwire.strada.BridgeComponent +import dev.hotwire.strada.BridgeDelegate +import dev.hotwire.strada.Message +import dev.hotwire.turbo.demo.R +import dev.hotwire.turbo.demo.base.NavDestination +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Bridge component to display a native 3-dot menu in the toolbar, which + * will will notify the web when it has been tapped. + */ +class OverflowMenuComponent( + name: String, + private val delegate: BridgeDelegate +) : BridgeComponent(name, delegate) { + + private val fragment: Fragment + get() = delegate.destination.fragment + private val toolbar: Toolbar? + get() = fragment.view?.findViewById(R.id.toolbar) + + override fun onReceive(message: Message) { + when (message.event) { + "connect" -> handleConnectEvent(message) + else -> Log.w("TurboDemo", "Unknown event for message: $message") + } + } + + private fun handleConnectEvent(message: Message) { + val data = message.data() ?: return + showOverflowMenuItem(data) + } + + private fun showOverflowMenuItem(data: MessageData) { + val toolbar = toolbar ?: return + + toolbar.menu.findItem(R.id.overflow)?.apply { + isVisible = true + title = data.label + } + + toolbar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.overflow -> { + performClick() + true + } + else -> false + } + } + } + + private fun performClick() { + replyTo("connect") + } + + @Serializable + data class MessageData( + @SerialName("label") val label: String + ) +} diff --git a/demo/src/main/res/drawable/ic_overflow.xml b/demo/src/main/res/drawable/ic_overflow.xml new file mode 100644 index 00000000..568cbb4d --- /dev/null +++ b/demo/src/main/res/drawable/ic_overflow.xml @@ -0,0 +1,9 @@ + + + diff --git a/demo/src/main/res/menu/web.xml b/demo/src/main/res/menu/web.xml index a96f5cda..0452d0a5 100644 --- a/demo/src/main/res/menu/web.xml +++ b/demo/src/main/res/menu/web.xml @@ -11,4 +11,13 @@ app:showAsAction="always" tools:ignore="AlwaysShowAction" /> + + From 6f4bd8cc98d9cd0191c85a375cf67e79ef5f7eb9 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Tue, 19 Sep 2023 18:40:02 -0400 Subject: [PATCH 6/6] Use the public strada dependency --- demo/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/build.gradle b/demo/build.gradle index 33b40f16..7e7983a0 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -71,6 +71,7 @@ dependencies { implementation 'androidx.browser:browser:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation 'com.github.bumptech.glide:glide:4.15.1' + implementation 'dev.hotwire:strada:1.0.0-beta2' implementation project(':turbo') }